tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 252 253 See also: `SearchByTicker()`, `SearchInstruments()`. 254 """ 255 256 self.figi = "" 257 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 258 259 See also: `SearchByFIGI()`, `SearchInstruments()`. 260 """ 261 262 self.depth = 1 263 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 264 265 See also: `GetCurrentPrices()`. 266 """ 267 268 self.server = r"https://invest-public-api.tinkoff.ru/rest" 269 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 270 271 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 272 """ 273 274 uLogger.debug("Broker API server: {}".format(self.server)) 275 276 self.timeout = 15 277 """Server operations timeout in seconds. Default: `15`. 278 279 See also: `SendAPIRequest()`. 280 """ 281 282 self.headers = { 283 "Content-Type": "application/json", 284 "accept": "application/json", 285 "Authorization": "Bearer {}".format(self.token), 286 "x-app-name": "Tim55667757.TKSBrokerAPI", 287 } 288 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.body = None 294 """Request body which send to broker server. Default: `None`. 295 296 See also: `SendAPIRequest()`. 297 """ 298 299 self.historyFile = None 300 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 301 302 See also: `History()`. 303 """ 304 305 self.htmlHistoryFile = "index.html" 306 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 307 308 See also: `ShowHistoryChart()`. 309 """ 310 311 self.instrumentsFile = "instruments.md" 312 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 313 314 See also: `ShowInstrumentsInfo()`. 315 """ 316 317 self.searchResultsFile = "search-results.md" 318 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 319 320 See also: `SearchInstruments()`. 321 """ 322 323 self.pricesFile = "prices.md" 324 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 325 326 See also: `GetListOfPrices()`. 327 """ 328 329 self.infoFile = "info.md" 330 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 331 332 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 333 """ 334 335 self.bondsXLSXFile = "ext-bonds.xlsx" 336 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 337 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 338 339 See also: `ExtendBondsData()`. 340 """ 341 342 self.calendarFile = "calendar.md" 343 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 344 345 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 346 347 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 348 """ 349 350 self.overviewFile = "overview.md" 351 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 352 353 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 354 """ 355 356 self.overviewDigestFile = "overview-digest.md" 357 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 358 359 See also: `Overview()` with parameter `details="digest"`. 360 """ 361 362 self.overviewPositionsFile = "overview-positions.md" 363 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 364 365 See also: `Overview()` with parameter `details="positions"`. 366 """ 367 368 self.overviewOrdersFile = "overview-orders.md" 369 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 370 371 See also: `Overview()` with parameter `details="orders"`. 372 """ 373 374 self.overviewAnalyticsFile = "overview-analytics.md" 375 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 376 377 See also: `Overview()` with parameter `details="analytics"`. 378 """ 379 380 self.reportFile = "deals.md" 381 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 382 383 See also: `Deals()`. 384 """ 385 386 self.withdrawalLimitsFile = "limits.md" 387 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 388 389 See also: `OverviewLimits()` and `RequestLimits()`. 390 """ 391 392 self.userInfoFile = "user-info.md" 393 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 394 395 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 396 """ 397 398 self.userAccountsFile = "accounts.md" 399 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 400 401 See also: `OverviewAccounts()`, `RequestAccounts()`. 402 """ 403 404 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 405 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 406 407 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 408 409 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 410 """ 411 412 self.iList = None # init iList for raw instruments data 413 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 414 415 See also: `Listing()`, `DumpInstruments()`. 416 """ 417 418 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 419 if useCache: 420 if os.path.exists(self.iListDumpFile): 421 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 422 curTime = datetime.now(tzutc()) 423 424 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 425 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 426 427 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 428 429 else: 430 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 431 432 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 433 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 434 435 else: 436 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 437 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 438 439 else: 440 self.iList = self.Listing() # request new raw instruments data from broker server 441 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 442 443 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 444 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 445 446 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 447 """ 448 449 @staticmethod 450 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 451 """ 452 Parse JSON from response string. 453 454 :param rawData: this is a string with JSON-formatted text. 455 :param debug: if `True` then print more debug information. 456 :return: JSON (dictionary), parsed from server response string. 457 """ 458 if debug: 459 uLogger.debug("Raw text body:") 460 uLogger.debug(rawData) 461 462 responseJSON = json.loads(rawData) if rawData else {} 463 464 if debug: 465 uLogger.debug("JSON formatted:") 466 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 467 uLogger.debug(jsonLine) 468 469 return responseJSON 470 471 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 472 """ 473 Send GET or POST request to broker server and receive JSON object. 474 475 self.header: must be defining with dictionary of headers. 476 self.body: if define then used as request body. None by default. 477 self.timeout: global request timeout, 15 seconds by default. 478 :param url: url with REST request. 479 :param reqType: send "GET" or "POST" request. "GET" by default. 480 :param retry: how many times retry after first request if an 5xx server errors occurred. 481 :param pause: sleep time in seconds between retries. 482 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 483 :return: response JSON (dictionary) from broker. 484 """ 485 if reqType not in ("GET", "POST"): 486 uLogger.error("You can define request type: 'GET' or 'POST'!") 487 raise Exception("Incorrect value") 488 489 if debug: 490 uLogger.debug("Request parameters:") 491 uLogger.debug(" - REST API URL: {}".format(url)) 492 uLogger.debug(" - request type: {}".format(reqType)) 493 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 494 uLogger.debug(" - body: {}".format(self.body)) 495 496 # fast hack to avoid all operations with some tickers/FIGI 497 responseJSON = {} 498 oK = True 499 for item in self.exclude: 500 if item in url: 501 if debug: 502 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 503 504 oK = False 505 break 506 507 if oK: 508 counter = 0 509 response = None 510 errMsg = "" 511 512 while not response and counter <= retry: 513 if reqType == "GET": 514 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if reqType == "POST": 517 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 518 519 if debug: 520 uLogger.debug("Response:") 521 uLogger.debug(" - status code: {}".format(response.status_code)) 522 uLogger.debug(" - reason: {}".format(response.reason)) 523 uLogger.debug(" - body length: {}".format(len(response.text))) 524 uLogger.debug(" - headers: {}".format(response.headers)) 525 526 # Server returns some headers: 527 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 528 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 529 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 530 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 531 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 532 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 533 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 534 sleep(rateLimitWait) 535 536 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 537 if 400 <= response.status_code < 500: 538 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 539 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 540 counter = retry + 1 541 542 if 500 <= response.status_code < 600: 543 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 544 uLogger.debug(" - not oK, {}".format(errMsg)) 545 counter += 1 546 547 if counter <= retry: 548 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 549 sleep(pause) 550 551 responseJSON = self._ParseJSON(response.text) 552 553 if errMsg: 554 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 555 uLogger.error(" - not oK, {}".format(errMsg)) 556 557 return responseJSON 558 559 def _IUpdater(self, iType: str) -> tuple: 560 """ 561 Request instrument by type from server. See available API methods for instruments: 562 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 563 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 564 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 565 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 566 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 567 568 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 569 :return: tuple with iType name and list of available instruments of current type for defined user token. 570 """ 571 result = [] 572 573 if iType in TKS_INSTRUMENTS: 574 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 575 576 # all instruments have the same body in API v2 requests: 577 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 578 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 579 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 580 581 return iType, result 582 583 def _IWrapper(self, kwargs): 584 """ 585 Wrapper runs instrument's update method `_IUpdater()`. 586 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 587 """ 588 return self._IUpdater(**kwargs) 589 590 def Listing(self) -> dict: 591 """ 592 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 593 594 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 595 """ 596 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 597 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 598 599 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 600 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 601 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 602 603 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 604 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 605 poolUpdater.close() 606 607 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 608 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 609 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 610 611 # calculate minimum price increment (step) for all instruments and set up instrument's type: 612 for iType in iList.keys(): 613 for ticker in iList[iType]: 614 iList[iType][ticker]["type"] = iType 615 616 if "minPriceIncrement" in iList[iType][ticker].keys(): 617 iList[iType][ticker]["step"] = NanoToFloat( 618 iList[iType][ticker]["minPriceIncrement"]["units"], 619 iList[iType][ticker]["minPriceIncrement"]["nano"], 620 ) 621 622 else: 623 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 624 625 return iList 626 627 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 628 """ 629 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 630 631 See also: `DumpInstruments()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 635 """ 636 if self.iListDumpFile is None or not self.iListDumpFile: 637 uLogger.error("Output name of dump file must be defined!") 638 raise Exception("Filename required") 639 640 if not self.iList or forceUpdate: 641 self.iList = self.Listing() 642 643 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 644 645 # Save as XLSX with separated sheets for every type of instruments: 646 with pd.ExcelWriter( 647 path=xlsxDumpFile, 648 date_format=TKS_DATE_FORMAT, 649 datetime_format=TKS_DATE_TIME_FORMAT, 650 mode="w", 651 ) as writer: 652 for iType in TKS_INSTRUMENTS: 653 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 654 df = df[sorted(df)] # sorted by column names 655 df = df.applymap( 656 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 657 na_action="ignore", 658 ) # converting numbers from nano-type to float in every cell 659 df.to_excel( 660 writer, 661 sheet_name=iType, 662 encoding="UTF-8", 663 freeze_panes=(1, 1), 664 ) # saving as XLSX-file with freeze first row and column as headers 665 666 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 667 668 def DumpInstruments(self, forceUpdate: bool = True) -> str: 669 """ 670 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 671 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 672 673 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 674 675 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 676 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 677 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 678 """ 679 if self.iListDumpFile is None or not self.iListDumpFile: 680 uLogger.error("Output name of dump file must be defined!") 681 raise Exception("Filename required") 682 683 if not self.iList or forceUpdate: 684 self.iList = self.Listing() 685 686 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 687 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 688 fH.write(jsonDump) 689 690 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 691 692 return jsonDump 693 694 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 695 """ 696 Show information about one instrument defined by json data and prints it in Markdown format. 697 698 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 699 700 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 701 :param show: if `True` then also printing information about instrument and its current price. 702 :return: multilines text in Markdown format with information about one instrument. 703 """ 704 splitLine = "| | |\n" 705 infoText = "" 706 707 if iJSON is not None and iJSON and isinstance(iJSON, dict): 708 info = [ 709 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 710 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 711 "| Parameters | Values |\n", 712 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 713 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 714 "| Full name: | {:<54} |\n".format(iJSON["name"]), 715 ] 716 717 if "sector" in iJSON.keys() and iJSON["sector"]: 718 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 719 720 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 721 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 722 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 723 ))) 724 725 info.extend([ 726 splitLine, 727 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 728 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 729 ]) 730 731 if "isin" in iJSON.keys() and iJSON["isin"]: 732 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 733 734 if "classCode" in iJSON.keys(): 735 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 736 737 info.extend([ 738 splitLine, 739 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 740 splitLine, 741 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 742 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 743 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 744 ]) 745 746 if iJSON["figi"]: 747 self.figi = iJSON["figi"] 748 iJSON = iJSON | self.RequestTradingStatus() 749 750 info.extend([ 751 splitLine, 752 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 753 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 754 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 755 ]) 756 757 info.append(splitLine) 758 759 if "type" in iJSON.keys() and iJSON["type"]: 760 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 761 762 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 763 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 764 765 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 766 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 767 768 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 769 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 770 771 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 772 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 773 774 if "focusType" in iJSON.keys() and iJSON["focusType"]: 775 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 776 777 if "assetType" in iJSON.keys() and iJSON["assetType"]: 778 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 779 780 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 781 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 782 783 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 784 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 785 786 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 787 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 788 789 if "currency" in iJSON.keys(): 790 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 791 792 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 793 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 794 795 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 796 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 799 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 800 801 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 802 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 803 804 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 805 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 806 807 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 808 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 809 810 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 811 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 812 813 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 814 info.append("| Perpetual bond: | Yes |\n") 815 816 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 817 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 818 819 iExt = None 820 if iJSON["type"] == "Bonds": 821 info.extend([ 822 splitLine, 823 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 824 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 825 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 826 iJSON["nominal"]["currency"], 827 )), 828 ]) 829 830 if "floatingCouponFlag" in iJSON.keys(): 831 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 832 833 if "amortizationFlag" in iJSON.keys(): 834 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 835 836 info.append(splitLine) 837 838 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 839 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 840 841 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 842 843 info.extend([ 844 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 845 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 846 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 847 ]) 848 849 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 850 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 851 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 852 iJSON["aciValue"]["currency"] 853 ))) 854 855 if "currentPrice" in iJSON.keys(): 856 info.append(splitLine) 857 858 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 859 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 860 861 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 862 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 863 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 864 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 865 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 866 867 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 868 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 869 870 info.extend([ 871 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 872 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 873 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 874 )), 875 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 876 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 877 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 878 )), 879 "| Changes between last deal price and last close | {:<54} |\n".format( 880 "{:.2f}%{}".format( 881 iJSON["currentPrice"]["changes"], 882 " ({}{:.2f} {})".format( 883 "+" if bondChangesDelta > 0 else "", 884 bondChangesDelta, 885 aciCurrency 886 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 887 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 888 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 889 currency 890 ), 891 ) 892 ), 893 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 894 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 897 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 898 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 899 )), 900 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 901 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 904 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 905 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 906 )), 907 ]) 908 909 if "lot" in iJSON.keys(): 910 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 911 912 if "step" in iJSON.keys() and iJSON["step"] != 0: 913 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 914 915 # Add bond payment calendar: 916 if iJSON["type"] == "Bonds": 917 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 918 info.extend(["\n", strCalendar]) 919 920 infoText += "".join(info) 921 922 if show: 923 uLogger.info("{}".format(infoText)) 924 925 else: 926 uLogger.debug("{}".format(infoText)) 927 928 if self.infoFile is not None: 929 with open(self.infoFile, "w", encoding="UTF-8") as fH: 930 fH.write(infoText) 931 932 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 933 934 return infoText 935 936 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 937 """ 938 Search and return raw broker's information about instrument by its ticker. 939 `ticker` must be defined! If debug=True then print all debug messages. 940 941 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 942 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 943 :param debug: if `True` then print all debug console messages. 944 :return: JSON formatted data with information about instrument. 945 """ 946 tickerJSON = {} 947 if debug: 948 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 949 950 if not self.ticker: 951 uLogger.warning("self.ticker variable is not be empty!") 952 953 else: 954 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 955 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 956 raise Exception("Instrument not allowed") 957 958 if not self.iList: 959 self.iList = self.Listing() 960 961 if self.ticker in self.iList["Shares"].keys(): 962 tickerJSON = self.iList["Shares"][self.ticker] 963 if debug: 964 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 965 966 elif self.ticker in self.iList["Currencies"].keys(): 967 tickerJSON = self.iList["Currencies"][self.ticker] 968 if debug: 969 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Bonds"].keys(): 972 tickerJSON = self.iList["Bonds"][self.ticker] 973 if debug: 974 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Etfs"].keys(): 977 tickerJSON = self.iList["Etfs"][self.ticker] 978 if debug: 979 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 980 981 elif self.ticker in self.iList["Futures"].keys(): 982 tickerJSON = self.iList["Futures"][self.ticker] 983 if debug: 984 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 985 986 if tickerJSON: 987 self.figi = tickerJSON["figi"] 988 989 if requestPrice: 990 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 991 992 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 993 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 994 995 else: 996 tickerJSON["currentPrice"]["changes"] = 0 997 998 if show: 999 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1000 1001 else: 1002 if show: 1003 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1004 1005 return tickerJSON 1006 1007 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1008 """ 1009 Search and return raw broker's information about instrument by its FIGI. 1010 `figi` must be defined! If debug=True then print all debug messages. 1011 1012 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1013 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1014 :param debug: if `True` then print all debug console messages. 1015 :return: JSON formatted data with information about instrument. 1016 """ 1017 figiJSON = {} 1018 if debug: 1019 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1020 1021 if not self.figi: 1022 uLogger.warning("self.figi variable is not be empty!") 1023 1024 else: 1025 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1026 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1027 raise Exception("Instrument not allowed") 1028 1029 if not self.iList: 1030 self.iList = self.Listing() 1031 1032 for item in self.iList["Shares"].keys(): 1033 if self.figi == self.iList["Shares"][item]["figi"]: 1034 figiJSON = self.iList["Shares"][item] 1035 1036 if debug: 1037 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1038 1039 break 1040 1041 if not figiJSON: 1042 for item in self.iList["Currencies"].keys(): 1043 if self.figi == self.iList["Currencies"][item]["figi"]: 1044 figiJSON = self.iList["Currencies"][item] 1045 1046 if debug: 1047 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1048 1049 break 1050 1051 if not figiJSON: 1052 for item in self.iList["Bonds"].keys(): 1053 if self.figi == self.iList["Bonds"][item]["figi"]: 1054 figiJSON = self.iList["Bonds"][item] 1055 1056 if debug: 1057 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1058 1059 break 1060 1061 if not figiJSON: 1062 for item in self.iList["Etfs"].keys(): 1063 if self.figi == self.iList["Etfs"][item]["figi"]: 1064 figiJSON = self.iList["Etfs"][item] 1065 1066 if debug: 1067 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1068 1069 break 1070 1071 if not figiJSON: 1072 for item in self.iList["Futures"].keys(): 1073 if self.figi == self.iList["Futures"][item]["figi"]: 1074 figiJSON = self.iList["Futures"][item] 1075 1076 if debug: 1077 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1078 1079 break 1080 1081 if figiJSON: 1082 self.figi = figiJSON["figi"] 1083 self.ticker = figiJSON["ticker"] 1084 1085 if requestPrice: 1086 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1087 1088 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1089 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1090 1091 else: 1092 figiJSON["currentPrice"]["changes"] = 0 1093 1094 if show: 1095 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1096 1097 else: 1098 if show: 1099 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1100 1101 return figiJSON 1102 1103 def GetCurrentPrices(self, show: bool = True) -> dict: 1104 """ 1105 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1106 `{"buy": [{"price": 1243.8, "quantity": 193}, 1107 {"price": 1244.0, "quantity": 168}, 1108 {"price": 1244.8, "quantity": 5}, 1109 {"price": 1245.0, "quantity": 61}, 1110 {"price": 1245.4, "quantity": 60}], 1111 "sell": [{"price": 1243.6, "quantity": 8}, 1112 {"price": 1242.6, "quantity": 10}, 1113 {"price": 1242.4, "quantity": 18}, 1114 {"price": 1242.2, "quantity": 50}, 1115 {"price": 1242.0, "quantity": 113}], 1116 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1117 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1118 - sell: list of dicts with Buyers prices, 1119 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1120 - quantity: volume value by current price in lots, 1121 - limitUp: current trade session limit price, maximum, 1122 - limitDown: current trade session limit price, minimum, 1123 - lastPrice: last deal price of the instrument, 1124 - closePrice: previous trade session close price of the instrument. 1125 1126 See also: `SearchByTicker()` and `SearchByFIGI()`. 1127 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1128 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1129 1130 :param show: if `True` then print DOM to log and console. 1131 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1132 If an error occurred then returns an empty record: 1133 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1134 """ 1135 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1136 1137 if self.depth < 1: 1138 uLogger.error("Depth of Market (DOM) must be >=1!") 1139 raise Exception("Incorrect value") 1140 1141 if not (self.ticker or self.figi): 1142 uLogger.error("self.ticker or self.figi variables must be defined!") 1143 raise Exception("Ticker or FIGI required") 1144 1145 if self.ticker and not self.figi: 1146 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1147 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1148 1149 if not self.ticker and self.figi: 1150 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1151 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1152 1153 if not self.figi: 1154 uLogger.error("FIGI is not defined!") 1155 raise Exception("Ticker or FIGI required") 1156 1157 else: 1158 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1159 1160 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1161 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1162 self.body = str({"figi": self.figi, "depth": self.depth}) 1163 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1164 1165 if pricesResponse: 1166 # list of dicts with sellers orders: 1167 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1168 1169 # list of dicts with buyers orders: 1170 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1171 1172 # max price of instrument at this time: 1173 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1174 1175 # min price of instrument at this time: 1176 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1177 1178 # last price of deal with instrument: 1179 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1180 1181 # last close price of instrument: 1182 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1183 1184 else: 1185 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1186 uLogger.debug("Server response: {}".format(pricesResponse)) 1187 1188 if show: 1189 if prices["buy"] or prices["sell"]: 1190 info = [ 1191 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1192 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1193 self.ticker, 1194 self.figi, 1195 self.depth, 1196 ), 1197 "-" * 60, "\n", 1198 " Orders of Buyers | Orders of Sellers\n", 1199 "-" * 60, "\n", 1200 " Sell prices (volumes) | Buy prices (volumes)\n", 1201 "-" * 60, "\n", 1202 ] 1203 1204 if not prices["buy"]: 1205 info.append(" | No orders!\n") 1206 sumBuy = 0 1207 1208 else: 1209 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1210 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1211 for item in maxMinSorted: 1212 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1213 1214 if not prices["sell"]: 1215 info.append("No orders! |\n") 1216 sumSell = 0 1217 1218 else: 1219 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1220 for item in prices["sell"]: 1221 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1222 1223 info.extend([ 1224 "-" * 60, "\n", 1225 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1226 "-" * 60, "\n", 1227 ]) 1228 1229 infoText = "".join(info) 1230 1231 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1232 1233 else: 1234 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1235 1236 return prices 1237 1238 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1239 """ 1240 This method get and show information about all available broker instruments for current user account. 1241 If `instrumentsFile` string is not empty then also save information to this file. 1242 1243 :param show: if `True` then print results to console, if `False` - print only to file. 1244 :return: multi-lines string with all available broker instruments 1245 """ 1246 if not self.iList: 1247 self.iList = self.Listing() 1248 1249 info = [ 1250 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1251 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1252 ] 1253 1254 # add instruments count by type: 1255 for iType in self.iList.keys(): 1256 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1257 1258 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1259 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1260 1261 # generating info tables with all instruments by type: 1262 for iType in self.iList.keys(): 1263 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1264 1265 for instrument in self.iList[iType].keys(): 1266 iName = self.iList[iType][instrument]["name"] # instrument's name 1267 if len(iName) > 57: 1268 iName = "{}...".format(iName[:54]) # right trim for a long string 1269 1270 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1271 self.iList[iType][instrument]["ticker"], 1272 iName, 1273 self.iList[iType][instrument]["figi"], 1274 self.iList[iType][instrument]["currency"], 1275 self.iList[iType][instrument]["lot"], 1276 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1277 )) 1278 1279 infoText = "".join(info) 1280 1281 if show: 1282 uLogger.info(infoText) 1283 1284 if self.instrumentsFile: 1285 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1286 fH.write(infoText) 1287 1288 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1289 1290 return infoText 1291 1292 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1293 """ 1294 This method search and show information about instruments by part of its ticker, FIGI or name. 1295 If `searchResultsFile` string is not empty then also save information to this file. 1296 1297 :param pattern: string with part of ticker, FIGI or instrument's name. 1298 :param show: if `True` then print results to console, if `False` - return list of result only. 1299 :return: list of dictionaries with all found instruments. 1300 """ 1301 if not self.iList: 1302 self.iList = self.Listing() 1303 1304 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1305 compiledPattern = re.compile(pattern, re.IGNORECASE) 1306 1307 for iType in self.iList: 1308 for instrument in self.iList[iType].values(): 1309 searchResult = compiledPattern.search(" ".join( 1310 [instrument["ticker"], instrument["figi"], instrument["name"]] 1311 )) 1312 1313 if searchResult: 1314 searchResults[iType][instrument["ticker"]] = instrument 1315 1316 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1317 info = [ 1318 "# Search results\n\n", 1319 "* **Search pattern:** [{}]\n".format(pattern), 1320 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1321 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1322 ] 1323 infoShort = info[:] 1324 1325 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1326 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1327 skippedLine = "| ... | ... | ... | ... |\n" 1328 1329 if resultsLen == 0: 1330 info.append("\nNo results\n") 1331 infoShort.append("\nNo results\n") 1332 uLogger.warning("No results. Try changing your search pattern.") 1333 1334 else: 1335 for iType in searchResults: 1336 iTypeValuesCount = len(searchResults[iType].values()) 1337 if iTypeValuesCount > 0: 1338 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 1341 for instrument in searchResults[iType].values(): 1342 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1343 instrument["type"], 1344 instrument["ticker"], 1345 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1346 instrument["figi"], 1347 )) 1348 1349 if iTypeValuesCount <= 5: 1350 infoShort.extend(info[-iTypeValuesCount:]) 1351 1352 else: 1353 infoShort.extend(info[-5:]) 1354 infoShort.append(skippedLine) 1355 1356 infoText = "".join(info) 1357 infoTextShort = "".join(infoShort) 1358 1359 if show: 1360 uLogger.info(infoTextShort) 1361 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1362 1363 if self.searchResultsFile: 1364 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1365 fH.write(infoText) 1366 1367 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1368 1369 return searchResults 1370 1371 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1372 """ 1373 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1374 1375 :param instruments: list of strings with tickers or FIGIs. 1376 :return: list with unique instrument FIGIs only. 1377 """ 1378 requestedInstruments = [] 1379 for iName in instruments: 1380 if iName not in self.aliases.keys(): 1381 if iName not in requestedInstruments: 1382 requestedInstruments.append(iName) 1383 1384 else: 1385 if iName not in requestedInstruments: 1386 if self.aliases[iName] not in requestedInstruments: 1387 requestedInstruments.append(self.aliases[iName]) 1388 1389 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1390 1391 onlyUniqueFIGIs = [] 1392 for iName in requestedInstruments: 1393 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1394 continue 1395 1396 self.ticker = iName 1397 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1398 1399 if not iData: 1400 self.ticker = "" 1401 self.figi = iName 1402 1403 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1404 1405 if not iData: 1406 self.figi = "" 1407 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1408 1409 if iData and iData["figi"] not in onlyUniqueFIGIs: 1410 onlyUniqueFIGIs.append(iData["figi"]) 1411 1412 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1413 1414 return onlyUniqueFIGIs 1415 1416 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1417 """ 1418 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1419 See limits: https://tinkoff.github.io/investAPI/limits/ 1420 If `pricesFile` string is not empty then also save information to this file. 1421 1422 :param instruments: list of strings with tickers or FIGIs. 1423 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1424 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1425 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1426 """ 1427 if instruments is None or not instruments: 1428 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1429 raise Exception("Ticker or FIGI required") 1430 1431 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1432 1433 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1434 1435 iList = [] # trying to get info and current prices about all unique instruments: 1436 for self.figi in onlyUniqueFIGIs: 1437 iData = self.SearchByFIGI(requestPrice=True) 1438 iList.append(iData) 1439 1440 self.ShowListOfPrices(iList, show) 1441 1442 return iList 1443 1444 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1445 """ 1446 Show table contains current prices of given instruments. 1447 1448 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1449 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1450 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1451 :return: multilines text in Markdown format as a table contains current prices. 1452 """ 1453 infoText = "" 1454 1455 if show or self.pricesFile: 1456 info = [ 1457 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1458 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1459 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1460 ] 1461 1462 for item in iList: 1463 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1464 item["ticker"], 1465 item["figi"], 1466 item["type"], 1467 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1468 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1469 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1470 "{} / {}".format( 1471 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1472 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1473 ), 1474 "{} / {}".format( 1475 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1476 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1477 ), 1478 item["currency"], 1479 )) 1480 1481 infoText = "".join(info) 1482 1483 if show: 1484 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1485 1486 if self.pricesFile: 1487 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1488 fH.write(infoText) 1489 1490 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1491 1492 return infoText 1493 1494 def RequestTradingStatus(self) -> dict: 1495 """ 1496 Requesting trading status for the instrument defined by `figi` variable. 1497 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1498 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1499 1500 :return: dictionary with trading status attributes. Response example: 1501 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1502 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1503 """ 1504 if self.figi is None or not self.figi: 1505 uLogger.error("Variable `figi` must be defined for using this method!") 1506 raise Exception("FIGI required") 1507 1508 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1509 1510 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1511 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1512 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1513 1514 uLogger.debug("Records about current trading status successfully received") 1515 1516 return tradingStatus 1517 1518 def RequestPortfolio(self) -> dict: 1519 """ 1520 Requesting actual user's portfolio for current `accountId`. 1521 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1522 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1523 1524 :return: dictionary with user's portfolio. 1525 """ 1526 if self.accountId is None or not self.accountId: 1527 uLogger.error("Variable `accountId` must be defined for using this method!") 1528 raise Exception("Account ID required") 1529 1530 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1531 1532 self.body = str({"accountId": self.accountId}) 1533 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1534 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1535 1536 uLogger.debug("Records about user's portfolio successfully received") 1537 1538 return rawPortfolio 1539 1540 def RequestPositions(self) -> dict: 1541 """ 1542 Requesting open positions by currencies and instruments for current `accountId`. 1543 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1544 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1545 1546 :return: dictionary with open positions by instruments. 1547 """ 1548 if self.accountId is None or not self.accountId: 1549 uLogger.error("Variable `accountId` must be defined for using this method!") 1550 raise Exception("Account ID required") 1551 1552 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1553 1554 self.body = str({"accountId": self.accountId}) 1555 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1556 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1557 1558 uLogger.debug("Records about current open positions successfully received") 1559 1560 return rawPositions 1561 1562 def RequestPendingOrders(self) -> list: 1563 """ 1564 Requesting current actual pending orders for current `accountId`. 1565 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1566 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1567 1568 :return: list of dictionaries with pending orders. 1569 """ 1570 if self.accountId is None or not self.accountId: 1571 uLogger.error("Variable `accountId` must be defined for using this method!") 1572 raise Exception("Account ID required") 1573 1574 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1575 1576 self.body = str({"accountId": self.accountId}) 1577 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1578 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1579 1580 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1581 1582 return rawOrders 1583 1584 def RequestStopOrders(self) -> list: 1585 """ 1586 Requesting current actual stop orders for current `accountId`. 1587 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1588 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1589 1590 :return: list of dictionaries with stop orders. 1591 """ 1592 if self.accountId is None or not self.accountId: 1593 uLogger.error("Variable `accountId` must be defined for using this method!") 1594 raise Exception("Account ID required") 1595 1596 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1597 1598 self.body = str({"accountId": self.accountId}) 1599 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1600 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1601 1602 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1603 1604 return rawStopOrders 1605 1606 def Overview(self, show: bool = False, details: str = "full") -> dict: 1607 """ 1608 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1609 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1610 are defined then also save information to file. 1611 1612 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1613 many requests about the state of the portfolio, and then, based on the received data, a large number 1614 of calculation and statistics are collected. 1615 1616 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1617 :param details: how detailed should the information be? You should specify one of strings: 1618 `full` - shows full available information about portfolio status (by default), 1619 `positions` - shows only open positions, 1620 `digest` - show a short digest of the portfolio status, 1621 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1622 `orders` - shows only sections of open limits and stop orders. 1623 :return: dictionary with client's raw portfolio and some statistics. 1624 """ 1625 if self.accountId is None or not self.accountId: 1626 uLogger.error("Variable `accountId` must be defined for using this method!") 1627 raise Exception("Account ID required") 1628 1629 view = { 1630 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1631 "headers": {}, # list of dictionaries, response headers without "positions" section 1632 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1633 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1634 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1635 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1636 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1637 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1638 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1639 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1640 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1641 }, 1642 "stat": { # --- some statistics calculated using "raw" sections: 1643 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1644 "availableRUB": 0., # available rubles (without other currencies) 1645 "blockedRUB": 0., # blocked sum in Russian Rouble 1646 "totalChangesRUB": 0., # changes for all open trades in RUB 1647 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1648 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1649 "sharesCostRUB": 0., # costs of all shares in RUB 1650 "bondsCostRUB": 0., # costs of all bonds in RUB 1651 "etfsCostRUB": 0., # costs of all etfs in RUB 1652 "futuresCostRUB": 0., # costs of all futures in RUB 1653 "Currencies": [], # list of dictionaries of all currencies statistics 1654 "Shares": [], # list of dictionaries of all shares statistics 1655 "Bonds": [], # list of dictionaries of all bonds statistics 1656 "Etfs": [], # list of dictionaries of all etfs statistics 1657 "Futures": [], # list of dictionaries of all futures statistics 1658 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1659 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1660 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1661 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1662 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1663 }, 1664 "analytics": { # --- some analytics of portfolio: 1665 "distrByAssets": {}, # portfolio distribution by assets 1666 "distrByCompanies": {}, # portfolio distribution by companies 1667 "distrBySectors": {}, # portfolio distribution by sectors 1668 "distrByCurrencies": {}, # portfolio distribution by currencies 1669 "distrByCountries": {}, # portfolio distribution by countries 1670 } 1671 } 1672 1673 details = details.lower() 1674 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1675 if details not in availableDetails: 1676 details = "full" 1677 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1678 1679 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1680 1681 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1682 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1683 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1684 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1685 1686 # save response headers without "positions" section: 1687 for key in portfolioResponse.keys(): 1688 if key != "positions": 1689 view["raw"]["headers"][key] = portfolioResponse[key] 1690 1691 else: 1692 continue 1693 1694 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1695 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1696 for item in portfolioResponse["positions"]: 1697 if item["instrumentType"] == "currency": 1698 self.figi = item["figi"] 1699 curr = self.SearchByFIGI(requestPrice=False) 1700 1701 # current price of currency in RUB: 1702 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1703 "name": curr["name"], 1704 "currentPrice": NanoToFloat( 1705 item["currentPrice"]["units"], 1706 item["currentPrice"]["nano"] 1707 ), 1708 } 1709 1710 view["raw"]["Currencies"].append(item) 1711 1712 elif item["instrumentType"] == "share": 1713 view["raw"]["Shares"].append(item) 1714 1715 elif item["instrumentType"] == "bond": 1716 view["raw"]["Bonds"].append(item) 1717 1718 elif item["instrumentType"] == "etf": 1719 view["raw"]["Etfs"].append(item) 1720 1721 elif item["instrumentType"] == "futures": 1722 view["raw"]["Futures"].append(item) 1723 1724 else: 1725 continue 1726 1727 # how many volume of currencies (by ISO currency name) are blocked: 1728 for item in view["raw"]["positions"]["blocked"]: 1729 blocked = NanoToFloat(item["units"], item["nano"]) 1730 if blocked > 0: 1731 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1732 1733 # how many volume of instruments (by FIGI) are blocked: 1734 for item in view["raw"]["positions"]["securities"]: 1735 blocked = int(item["blocked"]) 1736 if blocked > 0: 1737 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1738 1739 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1740 1741 if "rub" in allBlocked.keys(): 1742 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1743 1744 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1745 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1746 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1747 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1748 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1749 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1750 view["stat"]["portfolioCostRUB"] = sum([ 1751 view["stat"]["allCurrenciesCostRUB"], 1752 view["stat"]["sharesCostRUB"], 1753 view["stat"]["bondsCostRUB"], 1754 view["stat"]["etfsCostRUB"], 1755 view["stat"]["futuresCostRUB"], 1756 ]) 1757 1758 # --- calculating some portfolio statistics: 1759 byComp = {} # distribution by companies 1760 bySect = {} # distribution by sectors 1761 byCurr = {} # distribution by currencies (include RUB) 1762 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1763 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1764 1765 for item in portfolioResponse["positions"]: 1766 self.figi = item["figi"] 1767 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1768 1769 if instrument: 1770 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1771 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1772 1773 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1774 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1775 1776 else: 1777 blocked = 0 1778 1779 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1780 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1781 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1782 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1783 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1784 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1785 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1786 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1787 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1788 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1789 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1790 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1791 1792 statData = { 1793 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1794 "ticker": instrument["ticker"], # ticker by FIGI 1795 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1796 "volume": volume, # available volume of instrument 1797 "lots": lots, # volume in lots of instrument 1798 "direction": direction, # direction of an instrument's position: short or long 1799 "blocked": blocked, # blocked volume of currency or instrument 1800 "currentPrice": curPrice, # current instrument's price in basic asset 1801 "average": average, # current average position price 1802 "cost": cost, # current cost of all volume of instrument in basic asset 1803 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1804 "costRUB": costRUB, # cost of instrument in ruble 1805 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1806 "profit": profit, # expected profit at current moment 1807 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1808 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1809 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1810 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1811 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1812 "step": instrument["step"], # minimum price increment 1813 } 1814 1815 # adding distribution by unique countries: 1816 if statData["country"] not in byCountry.keys(): 1817 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1818 1819 else: 1820 byCountry[statData["country"]]["cost"] += costRUB 1821 byCountry[statData["country"]]["percent"] += percentCostRUB 1822 1823 if item["instrumentType"] != "currency": 1824 # adding distribution by unique companies: 1825 if statData["name"]: 1826 if statData["name"] not in byComp.keys(): 1827 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1828 1829 else: 1830 byComp[statData["name"]]["cost"] += costRUB 1831 byComp[statData["name"]]["percent"] += percentCostRUB 1832 1833 # adding distribution by unique sectors: 1834 if statData["sector"] not in bySect.keys(): 1835 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1836 1837 else: 1838 bySect[statData["sector"]]["cost"] += costRUB 1839 bySect[statData["sector"]]["percent"] += percentCostRUB 1840 1841 # adding distribution by unique currencies: 1842 if currency not in byCurr.keys(): 1843 byCurr[currency] = { 1844 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1845 "cost": costRUB, 1846 "percent": percentCostRUB 1847 } 1848 1849 else: 1850 byCurr[currency]["cost"] += costRUB 1851 byCurr[currency]["percent"] += percentCostRUB 1852 1853 # saving statistics for every instrument: 1854 if item["instrumentType"] == "currency": 1855 view["stat"]["Currencies"].append(statData) 1856 1857 # update dict with free funds for trading (total - blocked) by currencies 1858 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1859 view["stat"]["funds"][currency] = { 1860 "total": volume, 1861 "totalCostRUB": costRUB, # total volume cost in rubles 1862 "free": volume - blocked, 1863 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1864 } 1865 1866 elif item["instrumentType"] == "share": 1867 view["stat"]["Shares"].append(statData) 1868 1869 elif item["instrumentType"] == "bond": 1870 view["stat"]["Bonds"].append(statData) 1871 1872 elif item["instrumentType"] == "etf": 1873 view["stat"]["Etfs"].append(statData) 1874 1875 elif item["instrumentType"] == "Futures": 1876 view["stat"]["Futures"].append(statData) 1877 1878 else: 1879 continue 1880 1881 # total changes in Russian Ruble: 1882 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1883 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1884 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1885 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1886 view["stat"]["funds"]["rub"] = { 1887 "total": view["stat"]["availableRUB"], 1888 "totalCostRUB": view["stat"]["availableRUB"], 1889 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1890 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 } 1892 1893 # --- pending orders sector data: 1894 uniquePendingOrders = [] 1895 uniquePendingOrdersFIGIs = [] 1896 for item in view["raw"]["orders"]: 1897 if item["figi"] not in uniquePendingOrdersFIGIs: 1898 uniquePendingOrdersFIGIs.append(item["figi"]) 1899 uniquePendingOrders.append(item) 1900 1901 for item in uniquePendingOrders: 1902 self.figi = item["figi"] 1903 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1904 1905 if instrument: 1906 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1907 orderType = TKS_ORDER_TYPES[item["orderType"]] 1908 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1909 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1910 1911 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1912 if item["direction"] == "ORDER_DIRECTION_BUY": 1913 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1914 1915 else: 1916 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1917 1918 # requested price for order execution: 1919 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1920 1921 # necessary changes in percent to reach target from current price: 1922 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1923 1924 view["stat"]["orders"].append({ 1925 "orderID": item["orderId"], # orderId number parameter of current order 1926 "figi": item["figi"], # FIGI identification 1927 "ticker": instrument["ticker"], # ticker name by FIGI 1928 "lotsRequested": item["lotsRequested"], # requested lots value 1929 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1930 "currentPrice": lastPrice, # current instrument's price for defined action 1931 "targetPrice": target, # requested price for order execution in base currency 1932 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1933 "percentChanges": changes, # changes in percent to target from current price 1934 "currency": item["currency"], # instrument's currency name 1935 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1936 "type": orderType, # type of order from TKS_ORDER_TYPES 1937 "status": orderState, # order status from TKS_ORDER_STATES 1938 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1939 }) 1940 1941 # --- stop orders sector data: 1942 uniqueStopOrders = [] 1943 uniqueStopOrdersFIGIs = [] 1944 for item in view["raw"]["stopOrders"]: 1945 if item["figi"] not in uniqueStopOrdersFIGIs: 1946 uniqueStopOrdersFIGIs.append(item["figi"]) 1947 uniqueStopOrders.append(item) 1948 1949 for item in uniqueStopOrders: 1950 self.figi = item["figi"] 1951 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1952 1953 if instrument: 1954 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1955 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1956 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1957 1958 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1959 if "expirationTime" in item.keys(): 1960 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1961 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1962 1963 else: 1964 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1965 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1966 1967 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1968 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1969 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1970 1971 else: 1972 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1973 1974 # requested price when stop-order executed: 1975 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1976 1977 # price for limit-order, set up when stop-order executed: 1978 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1979 1980 # necessary changes in percent to reach target from current price: 1981 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1982 1983 view["stat"]["stopOrders"].append({ 1984 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1985 "figi": item["figi"], # FIGI identification 1986 "ticker": instrument["ticker"], # ticker name by FIGI 1987 "lotsRequested": item["lotsRequested"], # requested lots value 1988 "currentPrice": lastPrice, # current instrument's price for defined action 1989 "targetPrice": target, # requested price for stop-order execution in base currency 1990 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1991 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1992 "percentChanges": changes, # changes in percent to target from current price 1993 "currency": item["currency"], # instrument's currency name 1994 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1995 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1996 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1997 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1998 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1999 }) 2000 2001 # --- calculating data for analytics section: 2002 # portfolio distribution by assets: 2003 view["analytics"]["distrByAssets"] = { 2004 "Ruble": { 2005 "uniques": 1, 2006 "cost": view["stat"]["availableRUB"], 2007 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2008 }, 2009 "Currencies": { 2010 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2011 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2012 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 }, 2014 "Shares": { 2015 "uniques": len(view["stat"]["Shares"]), 2016 "cost": view["stat"]["sharesCostRUB"], 2017 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 }, 2019 "Bonds": { 2020 "uniques": len(view["stat"]["Bonds"]), 2021 "cost": view["stat"]["bondsCostRUB"], 2022 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2023 }, 2024 "Etfs": { 2025 "uniques": len(view["stat"]["Etfs"]), 2026 "cost": view["stat"]["etfsCostRUB"], 2027 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2028 }, 2029 "Futures": { 2030 "uniques": len(view["stat"]["Futures"]), 2031 "cost": view["stat"]["futuresCostRUB"], 2032 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 } 2035 2036 # portfolio distribution by companies: 2037 view["analytics"]["distrByCompanies"]["All money cash"] = { 2038 "ticker": "", 2039 "cost": view["stat"]["allCurrenciesCostRUB"], 2040 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 } 2042 view["analytics"]["distrByCompanies"].update(byComp) 2043 2044 # portfolio distribution by sectors: 2045 view["analytics"]["distrBySectors"]["All money cash"] = { 2046 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2047 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2048 } 2049 view["analytics"]["distrBySectors"].update(bySect) 2050 2051 # portfolio distribution by currencies: 2052 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2053 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2054 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2055 2056 view["analytics"]["distrByCurrencies"].update(byCurr) 2057 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2058 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2059 2060 # portfolio distribution by countries: 2061 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2062 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2063 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2064 2065 view["analytics"]["distrByCountries"].update(byCountry) 2066 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2068 2069 # --- Prepare text statistics overview in human-readable: 2070 if show: 2071 # Whatever the value `details`, header not changes: 2072 info = [ 2073 "# Client's portfolio\n\n", 2074 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2075 "* **Account ID:** [{}]\n".format(self.accountId), 2076 ] 2077 2078 if details in ["full", "positions", "digest"]: 2079 info.extend([ 2080 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2081 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2082 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2083 view["stat"]["totalChangesRUB"], 2084 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2085 view["stat"]["totalChangesPercentRUB"], 2086 ), 2087 ]) 2088 2089 if details in ["full", "positions"]: 2090 info.extend([ 2091 "## Open positions\n\n", 2092 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2093 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2094 "| Ruble | {:>31} | | | | | |\n".format( 2095 "{:.2f} ({:.2f}) rub".format( 2096 view["stat"]["availableRUB"], 2097 view["stat"]["blockedRUB"], 2098 ) 2099 ) 2100 ]) 2101 2102 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2103 return [ 2104 "| | | | | | | |\n", 2105 "| {:<27} | | | | | {:>19} | |\n".format( 2106 noTradeStr if noTradeStr else typeStr, 2107 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2108 ), 2109 ] 2110 2111 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2112 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2113 "{} [{}]".format(data["ticker"], data["figi"]), 2114 "{:.2f} ({:.2f}) {}".format( 2115 data["volume"], 2116 data["blocked"], 2117 data["currency"], 2118 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2119 data["volume"], 2120 data["blocked"], 2121 ), 2122 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2123 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2124 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2126 "{}{:.2f} {} ({}{:.2f}%)".format( 2127 "+" if data["profit"] > 0 else "", 2128 data["profit"], data["baseCurrencyName"], 2129 "+" if data["percentProfit"] > 0 else "", 2130 data["percentProfit"], 2131 ), 2132 ) 2133 2134 # --- Show currencies section: 2135 if view["stat"]["Currencies"]: 2136 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2137 for item in view["stat"]["Currencies"]: 2138 info.append(_InfoStr(item, showCurrencyName=True)) 2139 2140 else: 2141 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2142 2143 # --- Show shares section: 2144 if view["stat"]["Shares"]: 2145 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2146 2147 for item in view["stat"]["Shares"]: 2148 info.append(_InfoStr(item)) 2149 2150 else: 2151 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2152 2153 # --- Show bonds section: 2154 if view["stat"]["Bonds"]: 2155 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2156 2157 for item in view["stat"]["Bonds"]: 2158 info.append(_InfoStr(item)) 2159 2160 else: 2161 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2162 2163 # --- Show etfs section: 2164 if view["stat"]["Etfs"]: 2165 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2166 2167 for item in view["stat"]["Etfs"]: 2168 info.append(_InfoStr(item)) 2169 2170 else: 2171 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2172 2173 # --- Show futures section: 2174 if view["stat"]["Futures"]: 2175 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2176 2177 for item in view["stat"]["Futures"]: 2178 info.append(_InfoStr(item)) 2179 2180 else: 2181 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2182 2183 if details in ["full", "orders"]: 2184 # --- Show pending orders section: 2185 if view["stat"]["orders"]: 2186 info.extend([ 2187 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2188 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2189 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2190 ]) 2191 2192 for item in view["stat"]["orders"]: 2193 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2194 "{} [{}]".format(item["ticker"], item["figi"]), 2195 item["orderID"], 2196 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2197 "{} {} ({}{:.2f}%)".format( 2198 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2199 item["baseCurrencyName"], 2200 "+" if item["percentChanges"] > 0 else "", 2201 float(item["percentChanges"]), 2202 ), 2203 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2204 item["action"], 2205 item["type"], 2206 item["date"], 2207 )) 2208 2209 else: 2210 info.append("\n## Total pending limit-orders: 0\n") 2211 2212 # --- Show stop orders section: 2213 if view["stat"]["stopOrders"]: 2214 info.extend([ 2215 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2216 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2217 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2218 ]) 2219 2220 for item in view["stat"]["stopOrders"]: 2221 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2222 "{} [{}]".format(item["ticker"], item["figi"]), 2223 item["orderID"], 2224 item["lotsRequested"], 2225 "{} {} ({}{:.2f}%)".format( 2226 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2227 item["baseCurrencyName"], 2228 "+" if item["percentChanges"] > 0 else "", 2229 float(item["percentChanges"]), 2230 ), 2231 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2232 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2233 item["action"], 2234 item["type"], 2235 item["expType"], 2236 item["createDate"], 2237 item["expDate"], 2238 )) 2239 2240 else: 2241 info.append("\n## Total stop-orders: 0\n") 2242 2243 if details in ["full", "analytics"]: 2244 # -- Show analytics section: 2245 if view["stat"]["portfolioCostRUB"] > 0: 2246 info.extend([ 2247 "\n# Analytics\n" 2248 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2249 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2250 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2251 view["stat"]["totalChangesRUB"], 2252 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2253 view["stat"]["totalChangesPercentRUB"], 2254 ), 2255 "\n## Portfolio distribution by assets\n" 2256 "\n| Type | Uniques | Percent | Current cost |\n", 2257 "|------------|---------|---------|--------------------|\n", 2258 ]) 2259 2260 for key in view["analytics"]["distrByAssets"].keys(): 2261 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2262 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2263 key, 2264 view["analytics"]["distrByAssets"][key]["uniques"], 2265 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2266 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2267 )) 2268 2269 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2270 info.extend([ 2271 "\n## Portfolio distribution by companies\n" 2272 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2273 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2274 ]) 2275 2276 for company in view["analytics"]["distrByCompanies"].keys(): 2277 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2278 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2279 info.append("| {} | {:<7} | {:<18} |\n".format( 2280 "{}{}{}".format( 2281 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2282 company, 2283 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2284 ), 2285 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2286 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2287 )) 2288 2289 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2290 info.extend([ 2291 "\n## Portfolio distribution by sectors\n" 2292 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2293 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2294 ]) 2295 2296 for sector in view["analytics"]["distrBySectors"].keys(): 2297 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2298 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2299 sector, 2300 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2301 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2302 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2303 )) 2304 2305 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2306 info.extend([ 2307 "\n## Portfolio distribution by currencies\n" 2308 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2309 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2310 ]) 2311 2312 for curr in view["analytics"]["distrByCurrencies"].keys(): 2313 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2314 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2315 info.append("| {} | {:<7} | {:<18} |\n".format( 2316 "[{}] {}{}".format( 2317 curr, 2318 view["analytics"]["distrByCurrencies"][curr]["name"], 2319 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2320 ), 2321 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2322 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2323 )) 2324 2325 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2326 info.extend([ 2327 "\n## Portfolio distribution by countries\n" 2328 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2329 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2330 ]) 2331 2332 for country in view["analytics"]["distrByCountries"].keys(): 2333 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2334 nameLen = len(country) 2335 info.append("| {} | {:<7} | {:<18} |\n".format( 2336 "{}{}".format( 2337 country, 2338 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2339 ), 2340 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2341 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2342 )) 2343 2344 infoText = "".join(info) 2345 2346 uLogger.info(infoText) 2347 2348 if details == "full" and self.overviewFile: 2349 filename = self.overviewFile 2350 2351 elif details == "digest" and self.overviewDigestFile: 2352 filename = self.overviewDigestFile 2353 2354 elif details == "positions" and self.overviewPositionsFile: 2355 filename = self.overviewPositionsFile 2356 2357 elif details == "orders" and self.overviewOrdersFile: 2358 filename = self.overviewOrdersFile 2359 2360 elif details == "analytics" and self.overviewAnalyticsFile: 2361 filename = self.overviewAnalyticsFile 2362 2363 else: 2364 filename = "" 2365 2366 if filename: 2367 with open(filename, "w", encoding="UTF-8") as fH: 2368 fH.write(infoText) 2369 2370 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2371 2372 return view 2373 2374 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2375 """ 2376 Returns history operations between two given dates for current `accountId`. 2377 If `reportFile` string is not empty then also save human-readable report. 2378 Shows some statistical data of closed positions. 2379 2380 :param start: see docstring in `GetDatesAsString()` method 2381 :param end: see docstring in `GetDatesAsString()` method 2382 :param show: if `True` then also prints all records to the console. 2383 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2384 :return: original list of dictionaries with history of deals records from API ("operations" key): 2385 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2386 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2387 """ 2388 if self.accountId is None or not self.accountId: 2389 uLogger.error("Variable `accountId` must be defined for using this method!") 2390 raise Exception("Account ID required") 2391 2392 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2393 2394 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2395 2396 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2397 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2398 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2399 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2400 customStat = {} # custom statistics in additional to responseJSON 2401 2402 # --- output report in human-readable format: 2403 if show or self.reportFile: 2404 splitLine1 = "| | | | | |\n" # Summary section 2405 splitLine2 = "| | | | | | | | |\n" # Operations section 2406 nextDay = "" 2407 2408 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2409 2410 if len(ops) > 0: 2411 customStat = { 2412 "opsCount": 0, # total operations count 2413 "buyCount": 0, # buy operations 2414 "sellCount": 0, # sell operations 2415 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2416 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2417 "payIn": {"rub": 0.}, # Deposit brokerage account 2418 "payOut": {"rub": 0.}, # Withdrawals 2419 "divs": {"rub": 0.}, # Dividends income 2420 "coupons": {"rub": 0.}, # Coupon's income 2421 "brokerCom": {"rub": 0.}, # Service commissions 2422 "serviceCom": {"rub": 0.}, # Service commissions 2423 "marginCom": {"rub": 0.}, # Margin commissions 2424 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2425 } 2426 2427 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2428 for item in ops: 2429 if item["state"] == "OPERATION_STATE_EXECUTED": 2430 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2431 2432 # count buy operations: 2433 if "_BUY" in item["operationType"]: 2434 customStat["buyCount"] += 1 2435 2436 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2437 customStat["buyTotal"][item["payment"]["currency"]] += payment 2438 2439 else: 2440 customStat["buyTotal"][item["payment"]["currency"]] = payment 2441 2442 # count sell operations: 2443 elif "_SELL" in item["operationType"]: 2444 customStat["sellCount"] += 1 2445 2446 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2447 customStat["sellTotal"][item["payment"]["currency"]] += payment 2448 2449 else: 2450 customStat["sellTotal"][item["payment"]["currency"]] = payment 2451 2452 # count incoming operations: 2453 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2454 if item["payment"]["currency"] in customStat["payIn"].keys(): 2455 customStat["payIn"][item["payment"]["currency"]] += payment 2456 2457 else: 2458 customStat["payIn"][item["payment"]["currency"]] = payment 2459 2460 # count withdrawals operations: 2461 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2462 if item["payment"]["currency"] in customStat["payOut"].keys(): 2463 customStat["payOut"][item["payment"]["currency"]] += payment 2464 2465 else: 2466 customStat["payOut"][item["payment"]["currency"]] = payment 2467 2468 # count dividends income: 2469 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2470 if item["payment"]["currency"] in customStat["divs"].keys(): 2471 customStat["divs"][item["payment"]["currency"]] += payment 2472 2473 else: 2474 customStat["divs"][item["payment"]["currency"]] = payment 2475 2476 # count coupon's income: 2477 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2478 if item["payment"]["currency"] in customStat["coupons"].keys(): 2479 customStat["coupons"][item["payment"]["currency"]] += payment 2480 2481 else: 2482 customStat["coupons"][item["payment"]["currency"]] = payment 2483 2484 # count broker commissions: 2485 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2486 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2487 customStat["brokerCom"][item["payment"]["currency"]] += payment 2488 2489 else: 2490 customStat["brokerCom"][item["payment"]["currency"]] = payment 2491 2492 # count service commissions: 2493 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2494 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2495 customStat["serviceCom"][item["payment"]["currency"]] += payment 2496 2497 else: 2498 customStat["serviceCom"][item["payment"]["currency"]] = payment 2499 2500 # count margin commissions: 2501 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2502 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2503 customStat["marginCom"][item["payment"]["currency"]] += payment 2504 2505 else: 2506 customStat["marginCom"][item["payment"]["currency"]] = payment 2507 2508 # count withholding taxes: 2509 elif "_TAX" in item["operationType"]: 2510 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2511 customStat["allTaxes"][item["payment"]["currency"]] += payment 2512 2513 else: 2514 customStat["allTaxes"][item["payment"]["currency"]] = payment 2515 2516 else: 2517 continue 2518 2519 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2520 2521 # --- view "Actions" lines: 2522 info.extend([ 2523 "| 1 | 2 | 3 | 4 | 5 |\n", 2524 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2525 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2526 "| | Buy: {:<22} | {:<28} | | |\n".format( 2527 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2528 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2529 ), 2530 "| | Sell: {:<21} | {:<28} | | |\n".format( 2531 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2532 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2533 ), 2534 ]) 2535 2536 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2537 for key in opsKeys: 2538 if key == "rub": 2539 continue 2540 2541 info.extend([ 2542 "| | | {:<28} | | |\n".format( 2543 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2544 ), 2545 "| | | {:<28} | | |\n".format( 2546 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2547 ), 2548 ]) 2549 2550 info.append(splitLine1) 2551 2552 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2553 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2554 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2555 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2558 ) 2559 2560 # --- view "Payments" lines: 2561 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2562 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2563 2564 for key in paymentsKeys: 2565 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2566 2567 info.append(splitLine1) 2568 2569 # --- view "Commissions and taxes" lines: 2570 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2571 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2572 2573 for key in comKeys: 2574 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2575 2576 info.append(splitLine1) 2577 2578 info.extend([ 2579 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2580 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2581 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2582 ]) 2583 2584 else: 2585 info.append("Broker returned no operations during this period\n") 2586 2587 # --- view "Operations" section: 2588 for item in ops: 2589 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2590 continue 2591 2592 else: 2593 self.figi = item["figi"] if item["figi"] else "" 2594 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2595 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2596 2597 # group of deals during one day: 2598 if nextDay and item["date"].split("T")[0] != nextDay: 2599 info.append(splitLine2) 2600 nextDay = "" 2601 2602 else: 2603 nextDay = item["date"].split("T")[0] # saving current day for splitting 2604 2605 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2606 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2607 self.figi if self.figi else "—", 2608 instrument["ticker"] if instrument else "—", 2609 instrument["type"] if instrument else "—", 2610 item["quantity"] if int(item["quantity"]) > 0 else "—", 2611 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2612 TKS_OPERATION_STATES[item["state"]], 2613 TKS_OPERATION_TYPES[item["operationType"]], 2614 )) 2615 2616 infoText = "".join(info) 2617 2618 if show: 2619 uLogger.info(infoText) 2620 2621 if self.reportFile: 2622 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2623 fH.write(infoText) 2624 2625 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2626 2627 return ops, customStat 2628 2629 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2630 """ 2631 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2632 2633 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2634 Warning! Broker server used ISO UTC time by default. 2635 2636 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2637 Also, `historyFile` used to update history with `onlyMissing` parameter. 2638 2639 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2640 2641 :param start: see docstring in `GetDatesAsString()` method. 2642 :param end: see docstring in `GetDatesAsString()` method. 2643 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2644 `"hour"`, `"day"`. Default: `"hour"`. 2645 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2646 False by default. Warning! History appends only from last candle to current time 2647 with always update last candle! 2648 :param csvSep: separator if csv-file is used, `,` by default. 2649 :param show: if `True` then also prints Pandas DataFrame to the console. 2650 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2651 `["date", "time", "open", "high", "low", "close", "volume"]`. 2652 """ 2653 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2654 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2655 history = None # empty pandas object for history 2656 2657 if interval not in TKS_CANDLE_INTERVALS.keys(): 2658 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2659 raise Exception("Incorrect value") 2660 2661 if not (self.ticker or self.figi): 2662 uLogger.error("Ticker or FIGI must be defined!") 2663 raise Exception("Ticker or FIGI required") 2664 2665 if self.ticker and not self.figi: 2666 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2667 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2668 2669 if self.figi and not self.ticker: 2670 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2671 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2672 2673 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2674 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2675 if interval.lower() != "day": 2676 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2677 2678 delta = dtEnd - dtStart # current UTC time minus last time in file 2679 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2680 2681 # calculate history length in candles: 2682 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2683 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2684 length += 1 # to avoid fraction time 2685 2686 # calculate data blocks count: 2687 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2688 2689 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2690 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2691 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2692 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2693 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2694 2695 tempOld = None # pandas object for old history, if --only-missing key present 2696 lastTime = None # datetime object of last old candle in file 2697 2698 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2699 uLogger.debug("--only-missing key present, add only last missing candles...") 2700 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2701 2702 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2703 2704 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2705 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2706 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2707 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2708 2709 # get last datetime object from last string in file or minus 1 delta if file is empty: 2710 if len(tempOld) > 0: 2711 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2712 2713 else: 2714 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2715 2716 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2717 2718 responseJSONs = [] # raw history blocks of data 2719 2720 blockEnd = dtEnd 2721 for item in range(blocks): 2722 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2723 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2724 2725 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2726 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2727 )) 2728 2729 if blockStart == blockEnd: 2730 uLogger.debug("Skipped this zero-length block...") 2731 2732 else: 2733 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2734 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2735 self.body = str({ 2736 "figi": self.figi, 2737 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2738 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2739 "interval": TKS_CANDLE_INTERVALS[interval][0] 2740 }) 2741 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2742 2743 if "code" in responseJSON.keys(): 2744 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2745 2746 else: 2747 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2748 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2749 2750 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2751 2752 blockEnd = blockStart 2753 2754 printCount = len(responseJSONs) # candles to show in console 2755 if responseJSONs: 2756 tempHistory = pd.DataFrame( 2757 data={ 2758 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2759 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2761 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2762 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2763 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2764 "volume": [int(item["volume"]) for item in responseJSONs], 2765 }, 2766 index=range(len(responseJSONs)), 2767 columns=["date", "time", "open", "high", "low", "close", "volume"], 2768 ) 2769 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2770 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2771 2772 # append only newest candles to old history if --only-missing key present: 2773 if onlyMissing and tempOld is not None and lastTime is not None: 2774 index = 0 # find start index in tempHistory data: 2775 2776 for i, item in tempHistory.iterrows(): 2777 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2778 2779 if curTime == lastTime: 2780 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2781 index = i 2782 printCount = index + 1 2783 break 2784 2785 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2786 2787 else: 2788 history = tempHistory # if no `--only-missing` key then load full data from server 2789 2790 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2791 2792 if history is not None and not history.empty: 2793 if show: 2794 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2795 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2796 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2797 )) 2798 2799 else: 2800 uLogger.warning("Received an empty candles history!") 2801 2802 if self.historyFile is not None: 2803 if history is not None and not history.empty: 2804 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2805 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2806 2807 else: 2808 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2809 2810 else: 2811 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2812 2813 return history 2814 2815 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2816 """ 2817 Load candles history from csv-file and return Pandas DataFrame object. 2818 2819 See also: `History()` and `ShowHistoryChart()` methods. 2820 2821 :param filePath: path to csv-file to open. 2822 """ 2823 loadedHistory = None # init candles data object 2824 2825 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2826 2827 if os.path.exists(filePath): 2828 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2829 2830 tfStr = self.priceModel.FormattedDelta( 2831 self.priceModel.timeframe, 2832 "{days} days {hours}h {minutes}m {seconds}s", 2833 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2834 self.priceModel.timeframe, 2835 "{hours}h {minutes}m {seconds}s", 2836 ) 2837 2838 if loadedHistory is not None and not loadedHistory.empty: 2839 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2840 len(loadedHistory), 2841 tfStr, 2842 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2843 ) 2844 2845 else: 2846 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2847 2848 else: 2849 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2850 2851 return loadedHistory 2852 2853 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2854 """ 2855 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2856 2857 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2858 Default: `index.html` (both for interact and non-interact candlesticks chart). 2859 2860 See also: `History()` and `LoadHistory()` methods. 2861 2862 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2863 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2864 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2865 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2866 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2867 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2868 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2869 """ 2870 if isinstance(candles, str): 2871 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2872 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2873 2874 elif isinstance(candles, pd.DataFrame): 2875 self.priceModel.prices = candles # set candles chain from variable 2876 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2877 2878 if "datetime" not in candles.columns: 2879 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2880 2881 else: 2882 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2883 raise Exception("Incorrect value") 2884 2885 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2886 2887 if interact: 2888 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2889 2890 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2891 2892 else: 2893 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2894 2895 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2896 2897 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2898 2899 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2900 """ 2901 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2902 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2903 2904 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2905 2906 :param operation: string "Buy" or "Sell". 2907 :param lots: volume, integer count of lots >= 1. 2908 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2909 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2910 :param expDate: string "Undefined" by default or local date in future, 2911 it is a string with format `%Y-%m-%d %H:%M:%S`. 2912 :return: JSON with response from broker server. 2913 """ 2914 if self.accountId is None or not self.accountId: 2915 uLogger.error("Variable `accountId` must be defined for using this method!") 2916 raise Exception("Account ID required") 2917 2918 if operation is None or not operation or operation not in ("Buy", "Sell"): 2919 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2920 raise Exception("Incorrect value") 2921 2922 if lots is None or lots < 1: 2923 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2924 lots = 1 2925 2926 if tp is None or tp < 0: 2927 tp = 0 2928 2929 if sl is None or sl < 0: 2930 sl = 0 2931 2932 if expDate is None or not expDate: 2933 expDate = "Undefined" 2934 2935 if not (self.ticker or self.figi): 2936 uLogger.error("Ticker or FIGI must be defined!") 2937 raise Exception("Ticker or FIGI required") 2938 2939 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2940 self.ticker = instrument["ticker"] 2941 self.figi = instrument["figi"] 2942 2943 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2944 2945 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2946 self.body = str({ 2947 "figi": self.figi, 2948 "quantity": str(lots), 2949 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2950 "accountId": str(self.accountId), 2951 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2952 }) 2953 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2954 2955 if "orderId" in response.keys(): 2956 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2957 operation, response["orderId"], 2958 self.ticker, self.figi, lots, 2959 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2960 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2961 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2962 )) 2963 2964 else: 2965 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2966 2967 if tp > 0: 2968 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2969 2970 if sl > 0: 2971 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2972 2973 return response 2974 2975 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2976 """ 2977 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2978 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2979 2980 See also: `Order()` and `Trade()` docstrings. 2981 2982 :param lots: volume, integer count of lots >= 1. 2983 :param tp: float > 0, take profit price of stop-order. 2984 :param sl: float > 0, stop loss price of stop-order. 2985 :param expDate: it's a local date in future. 2986 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2987 :return: JSON with response from broker server. 2988 """ 2989 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2990 2991 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2992 """ 2993 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2994 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2995 2996 See also: `Order()` and `Trade()` docstrings. 2997 2998 :param lots: volume, integer count of lots >= 1. 2999 :param tp: float > 0, take profit price of stop-order. 3000 :param sl: float > 0, stop loss price of stop-order. 3001 :param expDate: it's a local date in the future. 3002 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3003 :return: JSON with response from broker server. 3004 """ 3005 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3006 3007 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3008 """ 3009 Close position of given instruments. 3010 3011 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3012 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3013 This avoids unnecessary downloading data from the server. 3014 """ 3015 if instruments is None or not instruments: 3016 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3017 raise Exception("Ticker or FIGI required") 3018 3019 if isinstance(instruments, str): 3020 instruments = [instruments] 3021 3022 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3023 if uniqueInstruments: 3024 if portfolio is None or not portfolio: 3025 portfolio = self.Overview(show=False) 3026 3027 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3028 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3029 3030 for self.figi in uniqueInstruments: 3031 if self.figi not in allOpened: 3032 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3033 continue 3034 3035 # search open trade info about instrument by ticker: 3036 instrument = {} 3037 for iType in TKS_INSTRUMENTS: 3038 if instrument: 3039 break 3040 3041 for item in portfolio["stat"][iType]: 3042 if item["figi"] == self.figi: 3043 instrument = item 3044 break 3045 3046 if instrument: 3047 self.ticker = instrument["ticker"] 3048 self.figi = instrument["figi"] 3049 3050 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3051 self.ticker, 3052 self.figi, 3053 int(instrument["volume"]), 3054 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3055 )) 3056 3057 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3058 3059 if tradeLots > 0: 3060 if instrument["blocked"] > 0: 3061 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3062 instrument["blocked"], 3063 self.ticker, 3064 tradeLots, 3065 )) 3066 3067 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3068 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3069 3070 else: 3071 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3072 3073 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3074 """ 3075 Close all positions of given instruments with defined type. 3076 3077 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3078 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3079 This avoids unnecessary downloading data from the server. 3080 """ 3081 if iType not in TKS_INSTRUMENTS: 3082 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3083 3084 else: 3085 if portfolio is None or not portfolio: 3086 portfolio = self.Overview(show=False) 3087 3088 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3089 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3090 3091 if tickers and portfolio: 3092 self.CloseTrades(tickers, portfolio) 3093 3094 else: 3095 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3096 3097 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3098 """ 3099 Universal method to create market or limit orders with all available parameters for current `accountId`. 3100 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3101 3102 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3103 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3104 3105 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3106 then broker immediately open market order as you can do simple --buy or --sell operations! 3107 3108 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3109 When current price will go up or down to target price value then broker opens a limit order. 3110 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3111 3112 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3113 3114 :param operation: string "Buy" or "Sell". 3115 :param orderType: string "Limit" or "Stop". 3116 :param lots: volume, integer count of lots >= 1. 3117 :param targetPrice: target price > 0. This is open trade price for limit order. 3118 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3119 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3120 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3121 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3122 Stop loss order always executed by market price. 3123 :param expDate: string "Undefined" by default or local date in future. 3124 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3125 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3126 A limit order has no expiration date, it lasts until the end of the trading day. 3127 :return: JSON with response from broker server. 3128 """ 3129 if self.accountId is None or not self.accountId: 3130 uLogger.error("Variable `accountId` must be defined for using this method!") 3131 raise Exception("Account ID required") 3132 3133 if operation is None or not operation or operation not in ("Buy", "Sell"): 3134 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3135 raise Exception("Incorrect value") 3136 3137 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3138 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3139 raise Exception("Incorrect value") 3140 3141 if lots is None or lots < 1: 3142 uLogger.error("You must define trade volume > 0: integer count of lots!") 3143 raise Exception("Incorrect value") 3144 3145 if targetPrice is None or targetPrice <= 0: 3146 uLogger.error("Target price for limit-order must be greater than 0!") 3147 raise Exception("Incorrect value") 3148 3149 if limitPrice is None or limitPrice <= 0: 3150 limitPrice = targetPrice 3151 3152 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3153 stopType = "Limit" 3154 3155 if expDate is None or not expDate: 3156 expDate = "Undefined" 3157 3158 if not (self.ticker or self.figi): 3159 uLogger.error("Tocker or FIGI must be defined!") 3160 raise Exception("Ticker or FIGI required") 3161 3162 response = {} 3163 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3164 self.ticker = instrument["ticker"] 3165 self.figi = instrument["figi"] 3166 3167 if orderType == "Limit": 3168 uLogger.debug( 3169 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3170 self.ticker, self.figi, 3171 operation, lots, targetPrice, instrument["currency"], 3172 )) 3173 3174 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3175 self.body = str({ 3176 "figi": self.figi, 3177 "quantity": str(lots), 3178 "price": FloatToNano(targetPrice), 3179 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3180 "accountId": str(self.accountId), 3181 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3182 }) 3183 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3184 3185 if "orderId" in response.keys(): 3186 uLogger.info( 3187 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3188 response["orderId"], 3189 self.ticker, self.figi, 3190 operation, lots, targetPrice, instrument["currency"], 3191 )) 3192 3193 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3194 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3195 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3196 targetPrice, instrument["currency"], 3197 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3198 )) 3199 3200 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3201 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3202 targetPrice, instrument["currency"], 3203 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3204 )) 3205 3206 else: 3207 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3208 3209 if orderType == "Stop": 3210 uLogger.debug( 3211 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3212 self.ticker, self.figi, 3213 operation, lots, 3214 targetPrice, instrument["currency"], 3215 limitPrice, instrument["currency"], 3216 stopType, expDate, 3217 )) 3218 3219 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3220 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3221 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3222 3223 body = { 3224 "figi": self.figi, 3225 "quantity": str(lots), 3226 "price": FloatToNano(limitPrice), 3227 "stopPrice": FloatToNano(targetPrice), 3228 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3229 "accountId": str(self.accountId), 3230 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3231 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3232 } 3233 3234 if expDateUTC: 3235 body["expireDate"] = expDateUTC 3236 3237 self.body = str(body) 3238 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3239 3240 if "stopOrderId" in response.keys(): 3241 uLogger.info( 3242 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3243 response["stopOrderId"], 3244 self.ticker, self.figi, 3245 operation, lots, 3246 targetPrice, instrument["currency"], 3247 limitPrice, instrument["currency"], 3248 TKS_STOP_ORDER_TYPES[stopOrderType], 3249 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3250 )) 3251 3252 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3253 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3254 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3255 targetPrice, instrument["currency"], 3256 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3257 )) 3258 3259 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3260 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3261 targetPrice, instrument["currency"], 3262 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3263 )) 3264 3265 else: 3266 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3267 3268 return response 3269 3270 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3271 """ 3272 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3273 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3274 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3275 See also: `Order()` docstring. 3276 3277 :param lots: volume, integer count of lots >= 1. 3278 :param targetPrice: target price > 0. This is open trade price for limit order. 3279 :return: JSON with response from broker server. 3280 """ 3281 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3282 3283 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3284 """ 3285 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3286 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3287 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3288 target price value then broker opens a limit order. See also: `Order()` docstring. 3289 3290 :param lots: volume, integer count of lots >= 1. 3291 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3292 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3293 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3294 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3295 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3296 :param expDate: string "Undefined" by default or local date in future. 3297 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3298 This date is converting to UTC format for server. 3299 :return: JSON with response from broker server. 3300 """ 3301 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3302 3303 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3304 """ 3305 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3306 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3307 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3308 See also: `Order()` docstring. 3309 3310 :param lots: volume, integer count of lots >= 1. 3311 :param targetPrice: target price > 0. This is open trade price for limit order. 3312 :return: JSON with response from broker server. 3313 """ 3314 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3315 3316 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3317 """ 3318 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3319 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3320 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3321 target price value then broker opens a limit order. See also: `Order()` docstring. 3322 3323 :param lots: volume, integer count of lots >= 1. 3324 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3325 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3326 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3327 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3328 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3329 :param expDate: string "Undefined" by default or local date in future. 3330 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3331 This date is converting to UTC format for server. 3332 :return: JSON with response from broker server. 3333 """ 3334 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3335 3336 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3337 """ 3338 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3339 3340 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3341 :param allOrdersIDs: pre-received lists of all active pending orders. 3342 This avoids unnecessary downloading data from the server. 3343 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3344 """ 3345 if self.accountId is None or not self.accountId: 3346 uLogger.error("Variable `accountId` must be defined for using this method!") 3347 raise Exception("Account ID required") 3348 3349 if orderIDs: 3350 if allOrdersIDs is None or not allOrdersIDs: 3351 rawOrders = self.RequestPendingOrders() 3352 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3353 3354 if allStopOrdersIDs is None or not allStopOrdersIDs: 3355 rawStopOrders = self.RequestStopOrders() 3356 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3357 3358 for orderID in orderIDs: 3359 idInPendingOrders = orderID in allOrdersIDs 3360 idInStopOrders = orderID in allStopOrdersIDs 3361 3362 if not (idInPendingOrders or idInStopOrders): 3363 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3364 continue 3365 3366 else: 3367 if idInPendingOrders: 3368 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3369 3370 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3371 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3372 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3373 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3374 3375 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3376 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3377 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3378 3379 else: 3380 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3381 3382 elif idInStopOrders: 3383 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3384 3385 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3386 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3387 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3388 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3389 3390 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3391 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3392 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3393 3394 else: 3395 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3396 3397 else: 3398 continue 3399 3400 def CloseAllOrders(self) -> None: 3401 """ 3402 Gets a list of open pending and stop orders and cancel it all. 3403 """ 3404 rawOrders = self.RequestPendingOrders() 3405 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3406 lenOrders = len(allOrdersIDs) 3407 3408 rawStopOrders = self.RequestStopOrders() 3409 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3410 lenSOrders = len(allStopOrdersIDs) 3411 3412 if lenOrders > 0 or lenSOrders > 0: 3413 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3414 3415 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3416 3417 else: 3418 uLogger.info("Orders not found, nothing to cancel.") 3419 3420 def CloseAll(self, *args) -> None: 3421 """ 3422 Close all available (not blocked) opened trades and orders. 3423 3424 Also, you can select one or more keywords case-insensitive: 3425 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3426 3427 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3428 """ 3429 overview = self.Overview(show=False) # get all open trades info 3430 3431 if len(args) == 0: 3432 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3433 self.CloseAllOrders() # close all pending and stop orders 3434 3435 for iType in TKS_INSTRUMENTS: 3436 if iType != "Currencies": 3437 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3438 3439 else: 3440 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3441 lowerArgs = [x.lower() for x in args] 3442 3443 if "orders" in lowerArgs: 3444 self.CloseAllOrders() # close all pending and stop orders 3445 3446 for iType in TKS_INSTRUMENTS: 3447 if iType.lower() in lowerArgs and iType != "Currencies": 3448 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3449 3450 @staticmethod 3451 def ParseOrderParameters(operation, **inputParameters): 3452 """ 3453 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3454 3455 :param operation: string "Buy" or "Sell". 3456 :param inputParameters: this is dict of strings that looks like this 3457 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3458 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3459 "prices" key: one or more prices to open limit-orders 3460 Counts of values in lots and prices lists must be equals! 3461 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3462 """ 3463 # TODO: update order grid work with api v2 3464 pass 3465 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3466 # 3467 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3468 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3469 # raise Exception("Incorrect value") 3470 # 3471 # if "l" in inputParameters.keys(): 3472 # inputParameters["lots"] = inputParameters.pop("l") 3473 # 3474 # if "p" in inputParameters.keys(): 3475 # inputParameters["prices"] = inputParameters.pop("p") 3476 # 3477 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3478 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3479 # raise Exception("Incorrect value") 3480 # 3481 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3482 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3483 # 3484 # if len(lots) != len(prices): 3485 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3486 # raise Exception("Incorrect value") 3487 # 3488 # uLogger.debug("Extracted parameters for orders:") 3489 # uLogger.debug("lots = {}".format(lots)) 3490 # uLogger.debug("prices = {}".format(prices)) 3491 # 3492 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3493 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3494 # uLogger.debug("Order parameters: {}".format(result)) 3495 # 3496 # return result 3497 3498 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3499 """ 3500 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3501 3502 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3503 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3504 """ 3505 result = False 3506 msg = "Instrument not defined!" 3507 3508 if portfolio is None or not portfolio: 3509 portfolio = self.Overview(show=False) 3510 3511 if self.ticker: 3512 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3513 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3514 3515 for iType in TKS_INSTRUMENTS: 3516 for instrument in portfolio["stat"][iType]: 3517 if instrument["ticker"] == self.ticker: 3518 result = True 3519 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3520 break 3521 3522 elif self.figi: 3523 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3524 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3525 3526 for iType in TKS_INSTRUMENTS: 3527 for instrument in portfolio["stat"][iType]: 3528 if instrument["figi"] == self.figi: 3529 result = True 3530 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3531 break 3532 3533 else: 3534 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3535 3536 uLogger.debug(msg) 3537 3538 return result 3539 3540 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3541 """ 3542 Returns instrument is in the user's portfolio if it presents there. 3543 Instrument must be defined by `ticker` (highly priority) or `figi`. 3544 3545 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3546 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3547 """ 3548 result = None 3549 msg = "Instrument not defined!" 3550 3551 if portfolio is None or not portfolio: 3552 portfolio = self.Overview(show=False) 3553 3554 if self.ticker: 3555 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3556 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3557 3558 for iType in TKS_INSTRUMENTS: 3559 for instrument in portfolio["stat"][iType]: 3560 if instrument["ticker"] == self.ticker: 3561 result = instrument 3562 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3563 break 3564 3565 elif self.figi: 3566 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3567 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3568 3569 for iType in TKS_INSTRUMENTS: 3570 for instrument in portfolio["stat"][iType]: 3571 if instrument["figi"] == self.figi: 3572 result = instrument 3573 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3574 break 3575 3576 else: 3577 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3578 3579 uLogger.debug(msg) 3580 3581 return result 3582 3583 def RequestLimits(self) -> dict: 3584 """ 3585 Method for obtaining the available funds for withdrawal for current `accountId`. 3586 3587 See also: 3588 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3589 - `OverviewLimits()` method 3590 3591 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3592 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3593 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3594 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3595 """ 3596 if self.accountId is None or not self.accountId: 3597 uLogger.error("Variable `accountId` must be defined for using this method!") 3598 raise Exception("Account ID required") 3599 3600 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3601 3602 self.body = str({"accountId": self.accountId}) 3603 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3604 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3605 3606 uLogger.debug("Records about available funds for withdrawal successfully received") 3607 3608 return rawLimits 3609 3610 def OverviewLimits(self, show: bool = False) -> dict: 3611 """ 3612 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3613 3614 See also: `RequestLimits()`. 3615 3616 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3617 :return: dict with raw parsed data from server and some calculated statistics about it. 3618 """ 3619 if self.accountId is None or not self.accountId: 3620 uLogger.error("Variable `accountId` must be defined for using this method!") 3621 raise Exception("Account ID required") 3622 3623 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3624 3625 view = { 3626 "rawLimits": rawLimits, 3627 "limits": { # parsed data for every currency: 3628 "money": { # this is an array of portfolio currency positions 3629 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3630 }, 3631 "blocked": { # this is an array of blocked currency 3632 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3633 }, 3634 "blockedGuarantee": { # this is locked money under collateral for futures 3635 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3636 }, 3637 }, 3638 } 3639 3640 # --- Prepare text table with limits in human-readable format: 3641 if show: 3642 info = [ 3643 "# Withdrawal limits\n\n", 3644 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3645 "* **Account ID:** [{}]\n".format(self.accountId), 3646 ] 3647 3648 if view["limits"]["money"]: 3649 info.extend([ 3650 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3651 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3652 ]) 3653 3654 else: 3655 info.append("\nNo withdrawal limits\n") 3656 3657 for curr in view["limits"]["money"].keys(): 3658 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3659 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3660 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3661 3662 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3663 "[{}]".format(curr), 3664 "{:.2f}".format(view["limits"]["money"][curr]), 3665 "{:.2f}".format(availableMoney), 3666 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3667 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3668 ) 3669 3670 if curr == "rub": 3671 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3672 3673 else: 3674 info.append(infoStr) 3675 3676 infoText = "".join(info) 3677 3678 uLogger.info(infoText) 3679 3680 if self.withdrawalLimitsFile: 3681 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3682 fH.write(infoText) 3683 3684 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3685 3686 return view 3687 3688 def RequestAccounts(self) -> dict: 3689 """ 3690 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3691 3692 See also: 3693 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3694 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3695 - `OverviewUserInfo()` method 3696 3697 :return: dict with raw data from server that contains accounts info. Example of dict: 3698 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3699 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3700 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3701 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3702 """ 3703 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3704 3705 self.body = str({}) 3706 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3707 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3708 3709 uLogger.debug("Records about available accounts successfully received") 3710 3711 return rawAccounts 3712 3713 def RequestUserInfo(self) -> dict: 3714 """ 3715 Method for requesting common user's information. 3716 3717 See also: 3718 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3719 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3720 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3721 - `OverviewUserInfo()` method 3722 3723 :return: dict with raw data from server that contains user's information. Example of dict: 3724 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3725 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3726 """ 3727 uLogger.debug("Requesting common user's information. Wait, please...") 3728 3729 self.body = str({}) 3730 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3731 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3732 3733 uLogger.debug("Records about current user successfully received") 3734 3735 return rawUserInfo 3736 3737 def RequestMarginStatus(self, accountId: str = None) -> dict: 3738 """ 3739 Method for requesting margin calculation for defined account ID. 3740 3741 See also: 3742 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3743 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3744 - `OverviewUserInfo()` method 3745 3746 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3747 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3748 Example of responses: 3749 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3750 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3751 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3752 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3753 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3754 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3755 """ 3756 if accountId is None or not accountId: 3757 if self.accountId is None or not self.accountId: 3758 uLogger.error("Variable `accountId` must be defined for using this method!") 3759 raise Exception("Account ID required") 3760 3761 else: 3762 accountId = self.accountId # use `self.accountId` (main ID) by default 3763 3764 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3765 3766 self.body = str({"accountId": accountId}) 3767 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3768 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3769 3770 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3771 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3772 rawMargin = {} 3773 3774 else: 3775 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3776 3777 return rawMargin 3778 3779 def RequestTariffLimits(self) -> dict: 3780 """ 3781 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3782 3783 See also: 3784 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3785 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3786 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3787 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3788 - `OverviewUserInfo()` method 3789 3790 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3791 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3792 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3793 """ 3794 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3795 3796 self.body = str({}) 3797 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3798 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3799 3800 uLogger.debug("Records with limits of current tariff successfully received") 3801 3802 return rawTariffLimits 3803 3804 def RequestBondCoupons(self, iJSON: dict) -> dict: 3805 """ 3806 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3807 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3808 All dates are in UTC timezone. 3809 3810 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3811 Documentation: 3812 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3813 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3814 3815 See also: `ExtendBondsData()`. 3816 3817 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3818 If raw iJSON is not data of bond then server returns an error [400] with message: 3819 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3820 :return: dictionary with bond payment calendar. Response example 3821 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3822 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3823 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3824 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3825 """ 3826 if iJSON["figi"] is None or not iJSON["figi"]: 3827 uLogger.error("FIGI must be defined for using this method!") 3828 raise Exception("FIGI required") 3829 3830 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3831 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3832 3833 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3834 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3835 self.figi, 3836 startDate, 3837 endDate, 3838 )) 3839 3840 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3841 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3842 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3843 3844 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3845 uLogger.warning("Instrument type is not bond!") 3846 3847 else: 3848 uLogger.debug("Records about bond payment calendar successfully received") 3849 3850 return calendar 3851 3852 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3853 """ 3854 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3855 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3856 coupon yields, current yields and some statistics etc. 3857 3858 WARNING! This is too long operation if a lot of bonds requested from broker server. 3859 3860 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3861 3862 :param instruments: list of strings with tickers or FIGIs. 3863 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3864 for further used by data scientists or stock analytics. 3865 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3866 In XLSX-file and Pandas DataFrame fields mean: 3867 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3868 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3869 """ 3870 if instruments is None or not instruments: 3871 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3872 raise Exception("Ticker or FIGI required") 3873 3874 if isinstance(instruments, str): 3875 instruments = [instruments] 3876 3877 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3878 3879 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3880 3881 iCount = len(uniqueInstruments) 3882 tooLong = iCount >= 20 3883 if tooLong: 3884 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3885 3886 bonds = None 3887 for i, self.figi in enumerate(uniqueInstruments): 3888 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3889 3890 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3891 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3892 rawBond = self.SearchByFIGI(requestPrice=True) 3893 3894 # Widen raw data with UTC current time (iData["actualDateTime"]): 3895 actualDate = datetime.now(tzutc()) 3896 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3897 3898 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3899 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3900 3901 # Replace some values with human-readable: 3902 iData["nominalCurrency"] = iData["nominal"]["currency"] 3903 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3904 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3905 iData["aciCurrency"] = iData["aciValue"]["currency"] 3906 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3907 iData["issueSize"] = int(iData["issueSize"]) 3908 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3909 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3910 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3911 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3912 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3913 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3914 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3915 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3916 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3917 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3918 3919 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3920 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3921 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3922 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3923 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3924 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3925 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3926 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3927 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3928 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3929 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3930 3931 # Widen raw data with calendar data from `rawCalendar` values: 3932 calendarData = [] 3933 for item in iData["rawCalendar"]["events"]: 3934 calendarData.append({ 3935 "couponDate": item["couponDate"], 3936 "couponNumber": int(item["couponNumber"]), 3937 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3938 "payCurrency": item["payOneBond"]["currency"], 3939 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3940 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3941 "couponStartDate": item["couponStartDate"], 3942 "couponEndDate": item["couponEndDate"], 3943 "couponPeriod": item["couponPeriod"], 3944 }) 3945 3946 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3947 if "maturityDate" not in iData.keys(): 3948 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3949 3950 # Widen raw data with Coupon Rate. 3951 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3952 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3953 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3954 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3955 3956 # Widen raw data with Yield to Maturity (YTM) on current date. 3957 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3958 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3959 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3960 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3961 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3962 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3963 3964 iData["calendar"] = calendarData # adds calendar at the end 3965 3966 # Remove not used data: 3967 iData.pop("uid") 3968 iData.pop("positionUid") 3969 iData.pop("currentPrice") 3970 iData.pop("rawCalendar") 3971 3972 colNames = list(iData.keys()) 3973 if bonds is None: 3974 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3975 3976 else: 3977 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3978 3979 else: 3980 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3981 3982 processed = round(100 * (i + 1) / iCount, 1) 3983 if tooLong and processed % 5 == 0: 3984 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3985 3986 else: 3987 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3988 3989 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3990 3991 # Saving bonds from Pandas DataFrame to XLSX sheet: 3992 if xlsx and self.bondsXLSXFile: 3993 with pd.ExcelWriter( 3994 path=self.bondsXLSXFile, 3995 date_format=TKS_DATE_FORMAT, 3996 datetime_format=TKS_DATE_TIME_FORMAT, 3997 mode="w", 3998 ) as writer: 3999 bonds.to_excel( 4000 writer, 4001 sheet_name="Extended bonds data", 4002 index=True, 4003 encoding="UTF-8", 4004 freeze_panes=(1, 1), 4005 ) # saving as XLSX-file with freeze first row and column as headers 4006 4007 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4008 4009 return bonds 4010 4011 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4012 """ 4013 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4014 4015 WARNING! This is too long operation if a lot of bonds requested from broker server. 4016 4017 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4018 4019 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4020 extended information about bonds: main info, current prices, bond payment calendar, 4021 coupon yields, current yields and some statistics etc. 4022 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4023 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4024 for further used by data scientists or stock analytics. 4025 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4026 """ 4027 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4028 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4029 4030 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4031 4032 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4033 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4034 calendar = None 4035 for bond in extBonds.iterrows(): 4036 for item in bond[1]["calendar"]: 4037 cData = { 4038 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4039 "couponDate": item["couponDate"], 4040 "figi": bond[1]["figi"], 4041 "ticker": bond[1]["ticker"], 4042 "name": bond[1]["name"], 4043 "couponNumber": item["couponNumber"], 4044 "payOneBond": item["payOneBond"], 4045 "payCurrency": item["payCurrency"], 4046 "couponType": item["couponType"], 4047 "couponPeriod": item["couponPeriod"], 4048 "fixDate": item["fixDate"], 4049 "couponStartDate": item["couponStartDate"], 4050 "couponEndDate": item["couponEndDate"], 4051 } 4052 4053 if calendar is None: 4054 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4055 4056 else: 4057 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4058 4059 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4060 4061 # Saving calendar from Pandas DataFrame to XLSX sheet: 4062 if xlsx: 4063 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4064 4065 with pd.ExcelWriter( 4066 path=xlsxCalendarFile, 4067 date_format=TKS_DATE_FORMAT, 4068 datetime_format=TKS_DATE_TIME_FORMAT, 4069 mode="w", 4070 ) as writer: 4071 humanReadable = calendar.copy(deep=True) 4072 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4073 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4074 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4075 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4076 humanReadable.columns = colNames # human-readable column names 4077 4078 humanReadable.to_excel( 4079 writer, 4080 sheet_name="Bond payments calendar", 4081 index=False, 4082 encoding="UTF-8", 4083 freeze_panes=(1, 2), 4084 ) # saving as XLSX-file with freeze first row and column as headers 4085 4086 del humanReadable # release df in memory 4087 4088 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4089 4090 return calendar 4091 4092 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4093 """ 4094 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4095 Also, creates Markdown file with calendar data, `calendar.md` by default. 4096 4097 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4098 4099 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4100 extended information about bonds: main info, current prices, bond payment calendar, 4101 coupon yields, current yields and some statistics etc. 4102 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4103 :param show: if `True` then also printing bonds payment calendar to the console, 4104 otherwise save to file `calendarFile` only. `False` by default. 4105 :return: multilines text in Markdown format with bonds payment calendar as a table. 4106 """ 4107 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4108 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4109 4110 infoText = "# Bond payments calendar\n\n" 4111 4112 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4113 4114 if not calendar.empty: 4115 splitLine = "| | | | | | | | | |\n" 4116 4117 info = [ 4118 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4119 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4120 ] 4121 4122 newMonth = False 4123 notOneBond = calendar["figi"].nunique() > 1 4124 for i, bond in enumerate(calendar.iterrows()): 4125 if newMonth and notOneBond: 4126 info.append(splitLine) 4127 4128 info.append( 4129 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4130 " √" if bond[1]["paid"] else " —", 4131 bond[1]["couponDate"].split("T")[0], 4132 bond[1]["figi"], 4133 bond[1]["ticker"], 4134 bond[1]["couponNumber"], 4135 "{} {}".format( 4136 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4137 bond[1]["payCurrency"], 4138 ), 4139 bond[1]["couponType"], 4140 bond[1]["couponPeriod"], 4141 bond[1]["fixDate"].split("T")[0], 4142 ) 4143 ) 4144 4145 if i < len(calendar.values) - 1: 4146 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4147 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4148 newMonth = False if curDate.month == nextDate.month else True 4149 4150 else: 4151 newMonth = False 4152 4153 infoText += "".join(info) 4154 4155 if show: 4156 uLogger.info("{}".format(infoText)) 4157 4158 if self.calendarFile is not None: 4159 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4160 fH.write(infoText) 4161 4162 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4163 4164 else: 4165 infoText += "No data\n" 4166 4167 return infoText 4168 4169 def OverviewAccounts(self, show: bool = False) -> dict: 4170 """ 4171 Method for parsing and show simple table with all available user accounts. 4172 4173 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4174 4175 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4176 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4177 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4178 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4179 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4180 "closed": "—", "access": "Full access" }, ...}}` 4181 """ 4182 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4183 4184 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4185 accounts = { 4186 item["id"]: { 4187 "type": TKS_ACCOUNT_TYPES[item["type"]], 4188 "name": item["name"], 4189 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4190 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4191 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4192 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4193 } for item in rawAccounts["accounts"] 4194 } 4195 4196 # Raw and parsed data with some fields replaced in "stat" section: 4197 view = { 4198 "rawAccounts": rawAccounts, 4199 "stat": accounts, 4200 } 4201 4202 # --- Prepare simple text table with only accounts data in human-readable format: 4203 if show: 4204 info = [ 4205 "# User accounts\n\n", 4206 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4207 "| Account ID | Type | Status | Name |\n", 4208 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4209 ] 4210 4211 for account in view["stat"].keys(): 4212 info.extend([ 4213 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4214 account, 4215 view["stat"][account]["type"], 4216 view["stat"][account]["status"], 4217 view["stat"][account]["name"], 4218 ) 4219 ]) 4220 4221 infoText = "".join(info) 4222 4223 uLogger.info(infoText) 4224 4225 if self.userAccountsFile: 4226 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4227 fH.write(infoText) 4228 4229 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4230 4231 return view 4232 4233 def OverviewUserInfo(self, show: bool = False) -> dict: 4234 """ 4235 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4236 4237 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4238 4239 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4240 :return: dict with raw parsed data from server and some calculated statistics about it. 4241 """ 4242 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4243 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4244 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4245 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4246 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4247 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4248 4249 # This is dict with parsed common user data: 4250 userInfo = { 4251 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4252 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4253 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4254 "tariff": rawUserInfo["tariff"], 4255 } 4256 4257 # This is an array of dict with parsed margin statuses for every account IDs: 4258 margins = {} 4259 for accountId in accounts.keys(): 4260 if rawMargins[accountId]: 4261 margins[accountId] = { 4262 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4263 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4264 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4265 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4266 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4267 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4268 } 4269 4270 else: 4271 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4272 4273 unary = {} # unary-connection limits 4274 for item in rawTariffLimits["unaryLimits"]: 4275 if item["limitPerMinute"] in unary.keys(): 4276 unary[item["limitPerMinute"]].extend(item["methods"]) 4277 4278 else: 4279 unary[item["limitPerMinute"]] = item["methods"] 4280 4281 stream = {} # stream-connection limits 4282 for item in rawTariffLimits["streamLimits"]: 4283 if item["limit"] in stream.keys(): 4284 stream[item["limit"]].extend(item["streams"]) 4285 4286 else: 4287 stream[item["limit"]] = item["streams"] 4288 4289 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4290 limits = { 4291 "unary": unary, 4292 "stream": stream, 4293 } 4294 4295 # Raw and parsed data as an output result: 4296 view = { 4297 "rawUserInfo": rawUserInfo, 4298 "rawAccounts": rawAccounts, 4299 "rawMargins": rawMargins, 4300 "rawTariffLimits": rawTariffLimits, 4301 "stat": { 4302 "userInfo": userInfo, 4303 "accounts": accounts, 4304 "margins": margins, 4305 "limits": limits, 4306 }, 4307 } 4308 4309 # --- Prepare text table with user information in human-readable format: 4310 if show: 4311 info = [ 4312 "# Full user information\n\n", 4313 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4314 "## Common information\n\n", 4315 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4316 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4317 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4318 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4319 "\n## User accounts\n\n", 4320 ] 4321 4322 for account in view["stat"]["accounts"].keys(): 4323 info.extend([ 4324 "### ID: [{}]\n\n".format(account), 4325 "| Parameters | Values |\n", 4326 "|----------------------|--------------------------------------------------------------|\n", 4327 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4328 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4329 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4330 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4331 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4332 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4333 ]) 4334 4335 if margins[account]: 4336 info.extend([ 4337 "| Margin status: | Enabled |\n", 4338 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4339 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4340 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4341 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4342 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4343 ]) 4344 4345 else: 4346 info.append("| Margin status: | Disabled |\n\n") 4347 4348 info.extend([ 4349 "\n## Current user tariff limits\n", 4350 "\nSee also:\n", 4351 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4352 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4353 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4354 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4355 "\n### Unary limits\n", 4356 ]) 4357 4358 if unary: 4359 for key, values in sorted(unary.items()): 4360 info.append("\n* Max requests per minute: {}\n".format(key)) 4361 4362 for value in values: 4363 info.append(" - {}\n".format(value)) 4364 4365 else: 4366 info.append("\nNot available\n") 4367 4368 info.append("\n### Stream limits\n") 4369 4370 if stream: 4371 for key, values in sorted(stream.items()): 4372 info.append("\n* Max stream connections: {}\n".format(key)) 4373 4374 for value in values: 4375 info.append(" - {}\n".format(value)) 4376 4377 else: 4378 info.append("\nNot available\n") 4379 4380 infoText = "".join(info) 4381 4382 uLogger.info(infoText) 4383 4384 if self.userInfoFile: 4385 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4386 fH.write(infoText) 4387 4388 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4389 4390 return view 4391 4392 4393class Args: 4394 """ 4395 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4396 """ 4397 def __init__(self, **kwargs): 4398 self.__dict__.update(kwargs) 4399 4400 def __getattr__(self, item): 4401 return None 4402 4403 4404def ParseArgs(): 4405 """This function get and parse command line keys.""" 4406 parser = ArgumentParser() # command-line string parser 4407 4408 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4409 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4410 4411 # --- options: 4412 4413 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4414 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4415 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4416 4417 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4418 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4419 4420 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4421 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4422 4423 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4424 4425 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4426 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4427 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4428 4429 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4430 4431 # --- commands: 4432 4433 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4434 4435 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4436 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4437 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4438 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4439 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4440 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4441 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4442 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4443 4444 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4445 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4446 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4447 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4448 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4449 4450 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4451 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4452 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4453 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4454 4455 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4456 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4457 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4458 4459 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4460 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4461 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4462 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4463 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4464 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4465 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4466 4467 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4468 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4469 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4470 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4471 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4472 4473 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4474 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4475 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4476 4477 cmdArgs = parser.parse_args() 4478 return cmdArgs 4479 4480 4481def Main(**kwargs): 4482 """ 4483 Main function for work with TKSBrokerAPI in the console. 4484 4485 See examples: 4486 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4487 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4488 """ 4489 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4490 4491 if args.debug_level: 4492 uLogger.level = 10 # always debug level by default 4493 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4494 4495 exitCode = 0 4496 start = datetime.now(tzutc()) 4497 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4498 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4499 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4500 )) 4501 4502 # trying to calculate full current version: 4503 buildVersion = __version__ 4504 try: 4505 v = version("tksbrokerapi") 4506 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4507 4508 except Exception: 4509 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4510 4511 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4512 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4513 4514 try: 4515 if args.version: 4516 print("TKSBrokerAPI {}".format(buildVersion)) 4517 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4518 4519 else: 4520 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4521 server = TinkoffBrokerServer( 4522 token=args.token, 4523 accountId=args.account_id, 4524 useCache=not args.no_cache, 4525 ) 4526 4527 # --- set some options: 4528 4529 if args.ticker: 4530 if args.ticker in server.aliasesKeys: 4531 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4532 4533 else: 4534 server.ticker = args.ticker 4535 4536 if args.figi: 4537 server.figi = args.figi 4538 4539 if args.depth is not None: 4540 server.depth = args.depth 4541 4542 # --- do one of commands: 4543 4544 if args.list: 4545 if args.output is not None: 4546 server.instrumentsFile = args.output 4547 4548 server.ShowInstrumentsInfo(show=True) 4549 4550 elif args.list_xlsx: 4551 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4552 4553 elif args.bonds_xlsx is not None: 4554 if args.output is not None: 4555 server.bondsXLSXFile = args.output 4556 4557 if len(args.bonds_xlsx) == 0: 4558 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4559 4560 else: 4561 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4562 4563 elif args.search: 4564 if args.output is not None: 4565 server.searchResultsFile = args.output 4566 4567 server.SearchInstruments(pattern=args.search[0], show=True) 4568 4569 elif args.info: 4570 if not (args.ticker or args.figi): 4571 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4572 raise Exception("Ticker or FIGI required") 4573 4574 if args.output is not None: 4575 server.infoFile = args.output 4576 4577 if args.ticker: 4578 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4579 4580 else: 4581 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4582 4583 elif args.calendar is not None: 4584 if args.output is not None: 4585 server.calendarFile = args.output 4586 4587 if len(args.calendar) == 0: 4588 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4589 4590 else: 4591 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4592 4593 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4594 4595 elif args.price: 4596 if not (args.ticker or args.figi): 4597 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4598 raise Exception("Ticker or FIGI required") 4599 4600 server.GetCurrentPrices(show=True) 4601 4602 elif args.prices is not None: 4603 if args.output is not None: 4604 server.pricesFile = args.output 4605 4606 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4607 4608 elif args.overview: 4609 if args.output is not None: 4610 server.overviewFile = args.output 4611 4612 server.Overview(show=True, details="full") 4613 4614 elif args.overview_digest: 4615 if args.output is not None: 4616 server.overviewDigestFile = args.output 4617 4618 server.Overview(show=True, details="digest") 4619 4620 elif args.overview_positions: 4621 if args.output is not None: 4622 server.overviewPositionsFile = args.output 4623 4624 server.Overview(show=True, details="positions") 4625 4626 elif args.overview_orders: 4627 if args.output is not None: 4628 server.overviewOrdersFile = args.output 4629 4630 server.Overview(show=True, details="orders") 4631 4632 elif args.overview_analytics: 4633 if args.output is not None: 4634 server.overviewAnalyticsFile = args.output 4635 4636 server.Overview(show=True, details="analytics") 4637 4638 elif args.deals is not None: 4639 if args.output is not None: 4640 server.reportFile = args.output 4641 4642 if 0 <= len(args.deals) < 3: 4643 server.Deals( 4644 start=args.deals[0] if len(args.deals) >= 1 else None, 4645 end=args.deals[1] if len(args.deals) == 2 else None, 4646 show=True, # Always show deals report in console 4647 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4648 ) 4649 4650 else: 4651 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4652 raise Exception("Incorrect value") 4653 4654 elif args.history is not None: 4655 if args.output is not None: 4656 server.historyFile = args.output 4657 4658 if 0 <= len(args.history) < 3: 4659 dataReceived = server.History( 4660 start=args.history[0] if len(args.history) >= 1 else None, 4661 end=args.history[1] if len(args.history) == 2 else None, 4662 interval="hour" if args.interval is None or not args.interval else args.interval, 4663 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4664 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4665 show=True, # shows all downloaded candles in console 4666 ) 4667 4668 if args.render_chart is not None and dataReceived is not None: 4669 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4670 4671 server.ShowHistoryChart( 4672 candles=dataReceived, 4673 interact=iChart, 4674 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4675 ) 4676 4677 else: 4678 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4679 raise Exception("Incorrect value") 4680 4681 elif args.load_history is not None: 4682 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4683 4684 if args.render_chart is not None and histData is not None: 4685 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4686 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4687 4688 server.ShowHistoryChart( 4689 candles=histData, 4690 interact=iChart, 4691 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4692 ) 4693 4694 elif args.trade is not None: 4695 if 1 <= len(args.trade) <= 5: 4696 server.Trade( 4697 operation=args.trade[0], 4698 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4699 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4700 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4701 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4702 ) 4703 4704 else: 4705 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4706 4707 elif args.buy is not None: 4708 if 0 <= len(args.buy) <= 4: 4709 server.Buy( 4710 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4711 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4712 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4713 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4714 ) 4715 4716 else: 4717 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4718 4719 elif args.sell is not None: 4720 if 0 <= len(args.sell) <= 4: 4721 server.Sell( 4722 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4723 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4724 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4725 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4726 ) 4727 4728 else: 4729 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4730 4731 elif args.order: 4732 if 4 <= len(args.order) <= 7: 4733 server.Order( 4734 operation=args.order[0], 4735 orderType=args.order[1], 4736 lots=int(args.order[2]), 4737 targetPrice=float(args.order[3]), 4738 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4739 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4740 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4741 ) 4742 4743 else: 4744 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4745 4746 elif args.buy_limit: 4747 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4748 4749 elif args.sell_limit: 4750 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4751 4752 elif args.buy_stop: 4753 if 2 <= len(args.buy_stop) <= 7: 4754 server.BuyStop( 4755 lots=int(args.buy_stop[0]), 4756 targetPrice=float(args.buy_stop[1]), 4757 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4758 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4759 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4760 ) 4761 4762 else: 4763 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4764 4765 elif args.sell_stop: 4766 if 2 <= len(args.sell_stop) <= 7: 4767 server.SellStop( 4768 lots=int(args.sell_stop[0]), 4769 targetPrice=float(args.sell_stop[1]), 4770 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4771 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4772 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4773 ) 4774 4775 else: 4776 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4777 4778 # elif args.buy_order_grid is not None: 4779 # # update order grid work with api v2 4780 # if len(args.buy_order_grid) == 2: 4781 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4782 # 4783 # for order in orderParams: 4784 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4785 # 4786 # else: 4787 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4788 # 4789 # elif args.sell_order_grid is not None: 4790 # # update order grid work with api v2 4791 # if len(args.sell_order_grid) >= 2: 4792 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4793 # 4794 # for order in orderParams: 4795 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4796 # 4797 # else: 4798 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4799 4800 elif args.close_order is not None: 4801 server.CloseOrders(args.close_order) # close only one order 4802 4803 elif args.close_orders is not None: 4804 server.CloseOrders(args.close_orders) # close list of orders 4805 4806 elif args.close_trade: 4807 if not (args.ticker or args.figi): 4808 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4809 raise Exception("Ticker or FIGI required") 4810 4811 if args.ticker: 4812 server.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4813 4814 else: 4815 server.CloseTrades([args.figi]) # close only one trade by FIGI 4816 4817 elif args.close_trades is not None: 4818 server.CloseTrades(args.close_trades) # close trades for list of tickers 4819 4820 elif args.close_all is not None: 4821 server.CloseAll(*args.close_all) 4822 4823 elif args.limits: 4824 if args.output is not None: 4825 server.withdrawalLimitsFile = args.output 4826 4827 server.OverviewLimits(show=True) 4828 4829 elif args.user_info: 4830 if args.output is not None: 4831 server.userInfoFile = args.output 4832 4833 server.OverviewUserInfo(show=True) 4834 4835 elif args.account: 4836 if args.output is not None: 4837 server.userAccountsFile = args.output 4838 4839 server.OverviewAccounts(show=True) 4840 4841 else: 4842 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4843 raise Exception("There is no command to execute") 4844 4845 except Exception: 4846 trace = tb.format_exc() 4847 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4848 if e in trace: 4849 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4850 break 4851 4852 uLogger.debug(trace) 4853 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4854 exitCode = 255 # an error occurred, must be open a ticket for this issue 4855 4856 finally: 4857 finish = datetime.now(tzutc()) 4858 4859 if exitCode == 0: 4860 uLogger.debug("All operations were finished success (summary code is 0).") 4861 4862 else: 4863 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4864 os.path.abspath(uLog.defaultLogFile), exitCode, 4865 )) 4866 4867 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4868 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4869 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4870 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4871 )) 4872 4873 if not kwargs: 4874 sys.exit(exitCode) 4875 4876 else: 4877 return exitCode 4878 4879 4880if __name__ == "__main__": 4881 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """ 449 450 @staticmethod 451 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 452 """ 453 Parse JSON from response string. 454 455 :param rawData: this is a string with JSON-formatted text. 456 :param debug: if `True` then print more debug information. 457 :return: JSON (dictionary), parsed from server response string. 458 """ 459 if debug: 460 uLogger.debug("Raw text body:") 461 uLogger.debug(rawData) 462 463 responseJSON = json.loads(rawData) if rawData else {} 464 465 if debug: 466 uLogger.debug("JSON formatted:") 467 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 468 uLogger.debug(jsonLine) 469 470 return responseJSON 471 472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON 559 560 def _IUpdater(self, iType: str) -> tuple: 561 """ 562 Request instrument by type from server. See available API methods for instruments: 563 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 564 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 565 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 566 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 567 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 568 569 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 570 :return: tuple with iType name and list of available instruments of current type for defined user token. 571 """ 572 result = [] 573 574 if iType in TKS_INSTRUMENTS: 575 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 576 577 # all instruments have the same body in API v2 requests: 578 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 579 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 580 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 581 582 return iType, result 583 584 def _IWrapper(self, kwargs): 585 """ 586 Wrapper runs instrument's update method `_IUpdater()`. 587 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 588 """ 589 return self._IUpdater(**kwargs) 590 591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList 627 628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 668 669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump 694 695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText 936 937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON 1007 1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON 1103 1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1107 `{"buy": [{"price": 1243.8, "quantity": 193}, 1108 {"price": 1244.0, "quantity": 168}, 1109 {"price": 1244.8, "quantity": 5}, 1110 {"price": 1245.0, "quantity": 61}, 1111 {"price": 1245.4, "quantity": 60}], 1112 "sell": [{"price": 1243.6, "quantity": 8}, 1113 {"price": 1242.6, "quantity": 10}, 1114 {"price": 1242.4, "quantity": 18}, 1115 {"price": 1242.2, "quantity": 50}, 1116 {"price": 1242.0, "quantity": 113}], 1117 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1118 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1119 - sell: list of dicts with Buyers prices, 1120 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1121 - quantity: volume value by current price in lots, 1122 - limitUp: current trade session limit price, maximum, 1123 - limitDown: current trade session limit price, minimum, 1124 - lastPrice: last deal price of the instrument, 1125 - closePrice: previous trade session close price of the instrument. 1126 1127 See also: `SearchByTicker()` and `SearchByFIGI()`. 1128 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1129 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1130 1131 :param show: if `True` then print DOM to log and console. 1132 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1133 If an error occurred then returns an empty record: 1134 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1135 """ 1136 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1137 1138 if self.depth < 1: 1139 uLogger.error("Depth of Market (DOM) must be >=1!") 1140 raise Exception("Incorrect value") 1141 1142 if not (self.ticker or self.figi): 1143 uLogger.error("self.ticker or self.figi variables must be defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 if self.ticker and not self.figi: 1147 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1148 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1149 1150 if not self.ticker and self.figi: 1151 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1153 1154 if not self.figi: 1155 uLogger.error("FIGI is not defined!") 1156 raise Exception("Ticker or FIGI required") 1157 1158 else: 1159 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1160 1161 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1162 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1163 self.body = str({"figi": self.figi, "depth": self.depth}) 1164 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1165 1166 if pricesResponse: 1167 # list of dicts with sellers orders: 1168 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1169 1170 # list of dicts with buyers orders: 1171 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1172 1173 # max price of instrument at this time: 1174 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1175 1176 # min price of instrument at this time: 1177 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1178 1179 # last price of deal with instrument: 1180 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1181 1182 # last close price of instrument: 1183 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1184 1185 else: 1186 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1187 uLogger.debug("Server response: {}".format(pricesResponse)) 1188 1189 if show: 1190 if prices["buy"] or prices["sell"]: 1191 info = [ 1192 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1193 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1194 self.ticker, 1195 self.figi, 1196 self.depth, 1197 ), 1198 "-" * 60, "\n", 1199 " Orders of Buyers | Orders of Sellers\n", 1200 "-" * 60, "\n", 1201 " Sell prices (volumes) | Buy prices (volumes)\n", 1202 "-" * 60, "\n", 1203 ] 1204 1205 if not prices["buy"]: 1206 info.append(" | No orders!\n") 1207 sumBuy = 0 1208 1209 else: 1210 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1211 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1212 for item in maxMinSorted: 1213 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1214 1215 if not prices["sell"]: 1216 info.append("No orders! |\n") 1217 sumSell = 0 1218 1219 else: 1220 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1221 for item in prices["sell"]: 1222 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1223 1224 info.extend([ 1225 "-" * 60, "\n", 1226 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1227 "-" * 60, "\n", 1228 ]) 1229 1230 infoText = "".join(info) 1231 1232 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1233 1234 else: 1235 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1236 1237 return prices 1238 1239 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1240 """ 1241 This method get and show information about all available broker instruments for current user account. 1242 If `instrumentsFile` string is not empty then also save information to this file. 1243 1244 :param show: if `True` then print results to console, if `False` - print only to file. 1245 :return: multi-lines string with all available broker instruments 1246 """ 1247 if not self.iList: 1248 self.iList = self.Listing() 1249 1250 info = [ 1251 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1252 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1253 ] 1254 1255 # add instruments count by type: 1256 for iType in self.iList.keys(): 1257 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1258 1259 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1260 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1261 1262 # generating info tables with all instruments by type: 1263 for iType in self.iList.keys(): 1264 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1265 1266 for instrument in self.iList[iType].keys(): 1267 iName = self.iList[iType][instrument]["name"] # instrument's name 1268 if len(iName) > 57: 1269 iName = "{}...".format(iName[:54]) # right trim for a long string 1270 1271 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1272 self.iList[iType][instrument]["ticker"], 1273 iName, 1274 self.iList[iType][instrument]["figi"], 1275 self.iList[iType][instrument]["currency"], 1276 self.iList[iType][instrument]["lot"], 1277 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1278 )) 1279 1280 infoText = "".join(info) 1281 1282 if show: 1283 uLogger.info(infoText) 1284 1285 if self.instrumentsFile: 1286 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1287 fH.write(infoText) 1288 1289 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1290 1291 return infoText 1292 1293 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` - return list of result only. 1300 :return: list of dictionaries with all found instruments. 1301 """ 1302 if not self.iList: 1303 self.iList = self.Listing() 1304 1305 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1306 compiledPattern = re.compile(pattern, re.IGNORECASE) 1307 1308 for iType in self.iList: 1309 for instrument in self.iList[iType].values(): 1310 searchResult = compiledPattern.search(" ".join( 1311 [instrument["ticker"], instrument["figi"], instrument["name"]] 1312 )) 1313 1314 if searchResult: 1315 searchResults[iType][instrument["ticker"]] = instrument 1316 1317 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1318 info = [ 1319 "# Search results\n\n", 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile: 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 return searchResults 1371 1372 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1373 """ 1374 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1375 1376 :param instruments: list of strings with tickers or FIGIs. 1377 :return: list with unique instrument FIGIs only. 1378 """ 1379 requestedInstruments = [] 1380 for iName in instruments: 1381 if iName not in self.aliases.keys(): 1382 if iName not in requestedInstruments: 1383 requestedInstruments.append(iName) 1384 1385 else: 1386 if iName not in requestedInstruments: 1387 if self.aliases[iName] not in requestedInstruments: 1388 requestedInstruments.append(self.aliases[iName]) 1389 1390 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1391 1392 onlyUniqueFIGIs = [] 1393 for iName in requestedInstruments: 1394 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1395 continue 1396 1397 self.ticker = iName 1398 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1399 1400 if not iData: 1401 self.ticker = "" 1402 self.figi = iName 1403 1404 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1405 1406 if not iData: 1407 self.figi = "" 1408 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1409 1410 if iData and iData["figi"] not in onlyUniqueFIGIs: 1411 onlyUniqueFIGIs.append(iData["figi"]) 1412 1413 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1414 1415 return onlyUniqueFIGIs 1416 1417 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1418 """ 1419 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 If `pricesFile` string is not empty then also save information to this file. 1422 1423 :param instruments: list of strings with tickers or FIGIs. 1424 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1425 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1427 """ 1428 if instruments is None or not instruments: 1429 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1430 raise Exception("Ticker or FIGI required") 1431 1432 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1433 1434 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1435 1436 iList = [] # trying to get info and current prices about all unique instruments: 1437 for self.figi in onlyUniqueFIGIs: 1438 iData = self.SearchByFIGI(requestPrice=True) 1439 iList.append(iData) 1440 1441 self.ShowListOfPrices(iList, show) 1442 1443 return iList 1444 1445 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1446 """ 1447 Show table contains current prices of given instruments. 1448 1449 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1451 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1452 :return: multilines text in Markdown format as a table contains current prices. 1453 """ 1454 infoText = "" 1455 1456 if show or self.pricesFile: 1457 info = [ 1458 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1459 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1460 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1461 ] 1462 1463 for item in iList: 1464 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1465 item["ticker"], 1466 item["figi"], 1467 item["type"], 1468 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1469 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1470 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1471 "{} / {}".format( 1472 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1473 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1474 ), 1475 "{} / {}".format( 1476 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1477 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1478 ), 1479 item["currency"], 1480 )) 1481 1482 infoText = "".join(info) 1483 1484 if show: 1485 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1486 1487 if self.pricesFile: 1488 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1489 fH.write(infoText) 1490 1491 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1492 1493 return infoText 1494 1495 def RequestTradingStatus(self) -> dict: 1496 """ 1497 Requesting trading status for the instrument defined by `figi` variable. 1498 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1499 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1500 1501 :return: dictionary with trading status attributes. Response example: 1502 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1503 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1504 """ 1505 if self.figi is None or not self.figi: 1506 uLogger.error("Variable `figi` must be defined for using this method!") 1507 raise Exception("FIGI required") 1508 1509 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1510 1511 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1512 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1513 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1514 1515 uLogger.debug("Records about current trading status successfully received") 1516 1517 return tradingStatus 1518 1519 def RequestPortfolio(self) -> dict: 1520 """ 1521 Requesting actual user's portfolio for current `accountId`. 1522 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1524 1525 :return: dictionary with user's portfolio. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1535 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1536 1537 uLogger.debug("Records about user's portfolio successfully received") 1538 1539 return rawPortfolio 1540 1541 def RequestPositions(self) -> dict: 1542 """ 1543 Requesting open positions by currencies and instruments for current `accountId`. 1544 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1545 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1546 1547 :return: dictionary with open positions by instruments. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1557 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1558 1559 uLogger.debug("Records about current open positions successfully received") 1560 1561 return rawPositions 1562 1563 def RequestPendingOrders(self) -> list: 1564 """ 1565 Requesting current actual pending orders for current `accountId`. 1566 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1580 1581 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1582 1583 return rawOrders 1584 1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1602 1603 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1604 1605 return rawStopOrders 1606 1607 def Overview(self, show: bool = False, details: str = "full") -> dict: 1608 """ 1609 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1610 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1611 are defined then also save information to file. 1612 1613 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1614 many requests about the state of the portfolio, and then, based on the received data, a large number 1615 of calculation and statistics are collected. 1616 1617 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1618 :param details: how detailed should the information be? You should specify one of strings: 1619 `full` - shows full available information about portfolio status (by default), 1620 `positions` - shows only open positions, 1621 `digest` - show a short digest of the portfolio status, 1622 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1623 `orders` - shows only sections of open limits and stop orders. 1624 :return: dictionary with client's raw portfolio and some statistics. 1625 """ 1626 if self.accountId is None or not self.accountId: 1627 uLogger.error("Variable `accountId` must be defined for using this method!") 1628 raise Exception("Account ID required") 1629 1630 view = { 1631 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1632 "headers": {}, # list of dictionaries, response headers without "positions" section 1633 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1634 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1635 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1636 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1637 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1638 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1639 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1640 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1641 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1642 }, 1643 "stat": { # --- some statistics calculated using "raw" sections: 1644 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1645 "availableRUB": 0., # available rubles (without other currencies) 1646 "blockedRUB": 0., # blocked sum in Russian Rouble 1647 "totalChangesRUB": 0., # changes for all open trades in RUB 1648 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1649 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1650 "sharesCostRUB": 0., # costs of all shares in RUB 1651 "bondsCostRUB": 0., # costs of all bonds in RUB 1652 "etfsCostRUB": 0., # costs of all etfs in RUB 1653 "futuresCostRUB": 0., # costs of all futures in RUB 1654 "Currencies": [], # list of dictionaries of all currencies statistics 1655 "Shares": [], # list of dictionaries of all shares statistics 1656 "Bonds": [], # list of dictionaries of all bonds statistics 1657 "Etfs": [], # list of dictionaries of all etfs statistics 1658 "Futures": [], # list of dictionaries of all futures statistics 1659 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1660 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1661 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1662 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1663 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1664 }, 1665 "analytics": { # --- some analytics of portfolio: 1666 "distrByAssets": {}, # portfolio distribution by assets 1667 "distrByCompanies": {}, # portfolio distribution by companies 1668 "distrBySectors": {}, # portfolio distribution by sectors 1669 "distrByCurrencies": {}, # portfolio distribution by currencies 1670 "distrByCountries": {}, # portfolio distribution by countries 1671 } 1672 } 1673 1674 details = details.lower() 1675 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1676 if details not in availableDetails: 1677 details = "full" 1678 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1679 1680 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1681 1682 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1683 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1684 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1685 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1686 1687 # save response headers without "positions" section: 1688 for key in portfolioResponse.keys(): 1689 if key != "positions": 1690 view["raw"]["headers"][key] = portfolioResponse[key] 1691 1692 else: 1693 continue 1694 1695 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1696 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1697 for item in portfolioResponse["positions"]: 1698 if item["instrumentType"] == "currency": 1699 self.figi = item["figi"] 1700 curr = self.SearchByFIGI(requestPrice=False) 1701 1702 # current price of currency in RUB: 1703 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1704 "name": curr["name"], 1705 "currentPrice": NanoToFloat( 1706 item["currentPrice"]["units"], 1707 item["currentPrice"]["nano"] 1708 ), 1709 } 1710 1711 view["raw"]["Currencies"].append(item) 1712 1713 elif item["instrumentType"] == "share": 1714 view["raw"]["Shares"].append(item) 1715 1716 elif item["instrumentType"] == "bond": 1717 view["raw"]["Bonds"].append(item) 1718 1719 elif item["instrumentType"] == "etf": 1720 view["raw"]["Etfs"].append(item) 1721 1722 elif item["instrumentType"] == "futures": 1723 view["raw"]["Futures"].append(item) 1724 1725 else: 1726 continue 1727 1728 # how many volume of currencies (by ISO currency name) are blocked: 1729 for item in view["raw"]["positions"]["blocked"]: 1730 blocked = NanoToFloat(item["units"], item["nano"]) 1731 if blocked > 0: 1732 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1733 1734 # how many volume of instruments (by FIGI) are blocked: 1735 for item in view["raw"]["positions"]["securities"]: 1736 blocked = int(item["blocked"]) 1737 if blocked > 0: 1738 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1739 1740 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1741 1742 if "rub" in allBlocked.keys(): 1743 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1744 1745 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1746 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1747 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1748 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1749 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1750 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1751 view["stat"]["portfolioCostRUB"] = sum([ 1752 view["stat"]["allCurrenciesCostRUB"], 1753 view["stat"]["sharesCostRUB"], 1754 view["stat"]["bondsCostRUB"], 1755 view["stat"]["etfsCostRUB"], 1756 view["stat"]["futuresCostRUB"], 1757 ]) 1758 1759 # --- calculating some portfolio statistics: 1760 byComp = {} # distribution by companies 1761 bySect = {} # distribution by sectors 1762 byCurr = {} # distribution by currencies (include RUB) 1763 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1764 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1765 1766 for item in portfolioResponse["positions"]: 1767 self.figi = item["figi"] 1768 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1769 1770 if instrument: 1771 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1772 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1773 1774 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1775 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1776 1777 else: 1778 blocked = 0 1779 1780 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1781 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1782 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1783 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1784 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1785 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1786 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1787 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1788 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1789 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1790 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1791 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1792 1793 statData = { 1794 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1795 "ticker": instrument["ticker"], # ticker by FIGI 1796 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1797 "volume": volume, # available volume of instrument 1798 "lots": lots, # volume in lots of instrument 1799 "direction": direction, # direction of an instrument's position: short or long 1800 "blocked": blocked, # blocked volume of currency or instrument 1801 "currentPrice": curPrice, # current instrument's price in basic asset 1802 "average": average, # current average position price 1803 "cost": cost, # current cost of all volume of instrument in basic asset 1804 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1805 "costRUB": costRUB, # cost of instrument in ruble 1806 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1807 "profit": profit, # expected profit at current moment 1808 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1809 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1810 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1811 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1812 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1813 "step": instrument["step"], # minimum price increment 1814 } 1815 1816 # adding distribution by unique countries: 1817 if statData["country"] not in byCountry.keys(): 1818 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1819 1820 else: 1821 byCountry[statData["country"]]["cost"] += costRUB 1822 byCountry[statData["country"]]["percent"] += percentCostRUB 1823 1824 if item["instrumentType"] != "currency": 1825 # adding distribution by unique companies: 1826 if statData["name"]: 1827 if statData["name"] not in byComp.keys(): 1828 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 byComp[statData["name"]]["cost"] += costRUB 1832 byComp[statData["name"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique sectors: 1835 if statData["sector"] not in bySect.keys(): 1836 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 bySect[statData["sector"]]["cost"] += costRUB 1840 bySect[statData["sector"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique currencies: 1843 if currency not in byCurr.keys(): 1844 byCurr[currency] = { 1845 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1846 "cost": costRUB, 1847 "percent": percentCostRUB 1848 } 1849 1850 else: 1851 byCurr[currency]["cost"] += costRUB 1852 byCurr[currency]["percent"] += percentCostRUB 1853 1854 # saving statistics for every instrument: 1855 if item["instrumentType"] == "currency": 1856 view["stat"]["Currencies"].append(statData) 1857 1858 # update dict with free funds for trading (total - blocked) by currencies 1859 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1860 view["stat"]["funds"][currency] = { 1861 "total": volume, 1862 "totalCostRUB": costRUB, # total volume cost in rubles 1863 "free": volume - blocked, 1864 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1865 } 1866 1867 elif item["instrumentType"] == "share": 1868 view["stat"]["Shares"].append(statData) 1869 1870 elif item["instrumentType"] == "bond": 1871 view["stat"]["Bonds"].append(statData) 1872 1873 elif item["instrumentType"] == "etf": 1874 view["stat"]["Etfs"].append(statData) 1875 1876 elif item["instrumentType"] == "Futures": 1877 view["stat"]["Futures"].append(statData) 1878 1879 else: 1880 continue 1881 1882 # total changes in Russian Ruble: 1883 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1884 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1885 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1886 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1887 view["stat"]["funds"]["rub"] = { 1888 "total": view["stat"]["availableRUB"], 1889 "totalCostRUB": view["stat"]["availableRUB"], 1890 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1892 } 1893 1894 # --- pending orders sector data: 1895 uniquePendingOrders = [] 1896 uniquePendingOrdersFIGIs = [] 1897 for item in view["raw"]["orders"]: 1898 if item["figi"] not in uniquePendingOrdersFIGIs: 1899 uniquePendingOrdersFIGIs.append(item["figi"]) 1900 uniquePendingOrders.append(item) 1901 1902 for item in uniquePendingOrders: 1903 self.figi = item["figi"] 1904 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrders = [] 1944 uniqueStopOrdersFIGIs = [] 1945 for item in view["raw"]["stopOrders"]: 1946 if item["figi"] not in uniqueStopOrdersFIGIs: 1947 uniqueStopOrdersFIGIs.append(item["figi"]) 1948 uniqueStopOrders.append(item) 1949 1950 for item in uniqueStopOrders: 1951 self.figi = item["figi"] 1952 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2055 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2056 2057 view["analytics"]["distrByCurrencies"].update(byCurr) 2058 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2059 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2060 2061 # portfolio distribution by countries: 2062 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2063 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2065 2066 view["analytics"]["distrByCountries"].update(byCountry) 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2068 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2069 2070 # --- Prepare text statistics overview in human-readable: 2071 if show: 2072 # Whatever the value `details`, header not changes: 2073 info = [ 2074 "# Client's portfolio\n\n", 2075 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2076 "* **Account ID:** [{}]\n".format(self.accountId), 2077 ] 2078 2079 if details in ["full", "positions", "digest"]: 2080 info.extend([ 2081 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2082 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2083 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2084 view["stat"]["totalChangesRUB"], 2085 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2086 view["stat"]["totalChangesPercentRUB"], 2087 ), 2088 ]) 2089 2090 if details in ["full", "positions"]: 2091 info.extend([ 2092 "## Open positions\n\n", 2093 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2094 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2095 "| Ruble | {:>31} | | | | | |\n".format( 2096 "{:.2f} ({:.2f}) rub".format( 2097 view["stat"]["availableRUB"], 2098 view["stat"]["blockedRUB"], 2099 ) 2100 ) 2101 ]) 2102 2103 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2104 return [ 2105 "| | | | | | | |\n", 2106 "| {:<27} | | | | | {:>19} | |\n".format( 2107 noTradeStr if noTradeStr else typeStr, 2108 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2109 ), 2110 ] 2111 2112 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2113 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2114 "{} [{}]".format(data["ticker"], data["figi"]), 2115 "{:.2f} ({:.2f}) {}".format( 2116 data["volume"], 2117 data["blocked"], 2118 data["currency"], 2119 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2120 data["volume"], 2121 data["blocked"], 2122 ), 2123 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2124 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2126 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2127 "{}{:.2f} {} ({}{:.2f}%)".format( 2128 "+" if data["profit"] > 0 else "", 2129 data["profit"], data["baseCurrencyName"], 2130 "+" if data["percentProfit"] > 0 else "", 2131 data["percentProfit"], 2132 ), 2133 ) 2134 2135 # --- Show currencies section: 2136 if view["stat"]["Currencies"]: 2137 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2138 for item in view["stat"]["Currencies"]: 2139 info.append(_InfoStr(item, showCurrencyName=True)) 2140 2141 else: 2142 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2143 2144 # --- Show shares section: 2145 if view["stat"]["Shares"]: 2146 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2147 2148 for item in view["stat"]["Shares"]: 2149 info.append(_InfoStr(item)) 2150 2151 else: 2152 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2153 2154 # --- Show bonds section: 2155 if view["stat"]["Bonds"]: 2156 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2157 2158 for item in view["stat"]["Bonds"]: 2159 info.append(_InfoStr(item)) 2160 2161 else: 2162 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2163 2164 # --- Show etfs section: 2165 if view["stat"]["Etfs"]: 2166 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2167 2168 for item in view["stat"]["Etfs"]: 2169 info.append(_InfoStr(item)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2173 2174 # --- Show futures section: 2175 if view["stat"]["Futures"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2177 2178 for item in view["stat"]["Futures"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2183 2184 if details in ["full", "orders"]: 2185 # --- Show pending orders section: 2186 if view["stat"]["orders"]: 2187 info.extend([ 2188 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2189 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2190 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2191 ]) 2192 2193 for item in view["stat"]["orders"]: 2194 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2195 "{} [{}]".format(item["ticker"], item["figi"]), 2196 item["orderID"], 2197 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2198 "{} {} ({}{:.2f}%)".format( 2199 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2200 item["baseCurrencyName"], 2201 "+" if item["percentChanges"] > 0 else "", 2202 float(item["percentChanges"]), 2203 ), 2204 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2205 item["action"], 2206 item["type"], 2207 item["date"], 2208 )) 2209 2210 else: 2211 info.append("\n## Total pending limit-orders: 0\n") 2212 2213 # --- Show stop orders section: 2214 if view["stat"]["stopOrders"]: 2215 info.extend([ 2216 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2217 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2218 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["stopOrders"]: 2222 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 item["lotsRequested"], 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2234 item["action"], 2235 item["type"], 2236 item["expType"], 2237 item["createDate"], 2238 item["expDate"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total stop-orders: 0\n") 2243 2244 if details in ["full", "analytics"]: 2245 # -- Show analytics section: 2246 if view["stat"]["portfolioCostRUB"] > 0: 2247 info.extend([ 2248 "\n# Analytics\n" 2249 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2250 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2251 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2252 view["stat"]["totalChangesRUB"], 2253 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2254 view["stat"]["totalChangesPercentRUB"], 2255 ), 2256 "\n## Portfolio distribution by assets\n" 2257 "\n| Type | Uniques | Percent | Current cost |\n", 2258 "|------------|---------|---------|--------------------|\n", 2259 ]) 2260 2261 for key in view["analytics"]["distrByAssets"].keys(): 2262 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2263 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2264 key, 2265 view["analytics"]["distrByAssets"][key]["uniques"], 2266 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2267 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2268 )) 2269 2270 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2271 info.extend([ 2272 "\n## Portfolio distribution by companies\n" 2273 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2274 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2275 ]) 2276 2277 for company in view["analytics"]["distrByCompanies"].keys(): 2278 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2279 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2280 info.append("| {} | {:<7} | {:<18} |\n".format( 2281 "{}{}{}".format( 2282 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2283 company, 2284 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2285 ), 2286 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2288 )) 2289 2290 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2291 info.extend([ 2292 "\n## Portfolio distribution by sectors\n" 2293 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2294 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2295 ]) 2296 2297 for sector in view["analytics"]["distrBySectors"].keys(): 2298 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2299 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2300 sector, 2301 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2302 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2304 )) 2305 2306 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2307 info.extend([ 2308 "\n## Portfolio distribution by currencies\n" 2309 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2310 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2311 ]) 2312 2313 for curr in view["analytics"]["distrByCurrencies"].keys(): 2314 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2315 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2316 info.append("| {} | {:<7} | {:<18} |\n".format( 2317 "[{}] {}{}".format( 2318 curr, 2319 view["analytics"]["distrByCurrencies"][curr]["name"], 2320 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2324 )) 2325 2326 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2327 info.extend([ 2328 "\n## Portfolio distribution by countries\n" 2329 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2330 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2331 ]) 2332 2333 for country in view["analytics"]["distrByCountries"].keys(): 2334 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2335 nameLen = len(country) 2336 info.append("| {} | {:<7} | {:<18} |\n".format( 2337 "{}{}".format( 2338 country, 2339 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2340 ), 2341 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2343 )) 2344 2345 infoText = "".join(info) 2346 2347 uLogger.info(infoText) 2348 2349 if details == "full" and self.overviewFile: 2350 filename = self.overviewFile 2351 2352 elif details == "digest" and self.overviewDigestFile: 2353 filename = self.overviewDigestFile 2354 2355 elif details == "positions" and self.overviewPositionsFile: 2356 filename = self.overviewPositionsFile 2357 2358 elif details == "orders" and self.overviewOrdersFile: 2359 filename = self.overviewOrdersFile 2360 2361 elif details == "analytics" and self.overviewAnalyticsFile: 2362 filename = self.overviewAnalyticsFile 2363 2364 else: 2365 filename = "" 2366 2367 if filename: 2368 with open(filename, "w", encoding="UTF-8") as fH: 2369 fH.write(infoText) 2370 2371 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2372 2373 return view 2374 2375 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2376 """ 2377 Returns history operations between two given dates for current `accountId`. 2378 If `reportFile` string is not empty then also save human-readable report. 2379 Shows some statistical data of closed positions. 2380 2381 :param start: see docstring in `GetDatesAsString()` method 2382 :param end: see docstring in `GetDatesAsString()` method 2383 :param show: if `True` then also prints all records to the console. 2384 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2385 :return: original list of dictionaries with history of deals records from API ("operations" key): 2386 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2387 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2388 """ 2389 if self.accountId is None or not self.accountId: 2390 uLogger.error("Variable `accountId` must be defined for using this method!") 2391 raise Exception("Account ID required") 2392 2393 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2394 2395 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2396 2397 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2398 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2399 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2400 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2401 customStat = {} # custom statistics in additional to responseJSON 2402 2403 # --- output report in human-readable format: 2404 if show or self.reportFile: 2405 splitLine1 = "| | | | | |\n" # Summary section 2406 splitLine2 = "| | | | | | | | |\n" # Operations section 2407 nextDay = "" 2408 2409 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2410 2411 if len(ops) > 0: 2412 customStat = { 2413 "opsCount": 0, # total operations count 2414 "buyCount": 0, # buy operations 2415 "sellCount": 0, # sell operations 2416 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2417 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2418 "payIn": {"rub": 0.}, # Deposit brokerage account 2419 "payOut": {"rub": 0.}, # Withdrawals 2420 "divs": {"rub": 0.}, # Dividends income 2421 "coupons": {"rub": 0.}, # Coupon's income 2422 "brokerCom": {"rub": 0.}, # Service commissions 2423 "serviceCom": {"rub": 0.}, # Service commissions 2424 "marginCom": {"rub": 0.}, # Margin commissions 2425 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2426 } 2427 2428 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2429 for item in ops: 2430 if item["state"] == "OPERATION_STATE_EXECUTED": 2431 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2432 2433 # count buy operations: 2434 if "_BUY" in item["operationType"]: 2435 customStat["buyCount"] += 1 2436 2437 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2438 customStat["buyTotal"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["buyTotal"][item["payment"]["currency"]] = payment 2442 2443 # count sell operations: 2444 elif "_SELL" in item["operationType"]: 2445 customStat["sellCount"] += 1 2446 2447 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2448 customStat["sellTotal"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["sellTotal"][item["payment"]["currency"]] = payment 2452 2453 # count incoming operations: 2454 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2455 if item["payment"]["currency"] in customStat["payIn"].keys(): 2456 customStat["payIn"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["payIn"][item["payment"]["currency"]] = payment 2460 2461 # count withdrawals operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2463 if item["payment"]["currency"] in customStat["payOut"].keys(): 2464 customStat["payOut"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payOut"][item["payment"]["currency"]] = payment 2468 2469 # count dividends income: 2470 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2471 if item["payment"]["currency"] in customStat["divs"].keys(): 2472 customStat["divs"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["divs"][item["payment"]["currency"]] = payment 2476 2477 # count coupon's income: 2478 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2479 if item["payment"]["currency"] in customStat["coupons"].keys(): 2480 customStat["coupons"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["coupons"][item["payment"]["currency"]] = payment 2484 2485 # count broker commissions: 2486 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2487 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2488 customStat["brokerCom"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["brokerCom"][item["payment"]["currency"]] = payment 2492 2493 # count service commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2495 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2496 customStat["serviceCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["serviceCom"][item["payment"]["currency"]] = payment 2500 2501 # count margin commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2503 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2504 customStat["marginCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["marginCom"][item["payment"]["currency"]] = payment 2508 2509 # count withholding taxes: 2510 elif "_TAX" in item["operationType"]: 2511 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2512 customStat["allTaxes"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["allTaxes"][item["payment"]["currency"]] = payment 2516 2517 else: 2518 continue 2519 2520 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2521 2522 # --- view "Actions" lines: 2523 info.extend([ 2524 "| 1 | 2 | 3 | 4 | 5 |\n", 2525 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2526 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2527 "| | Buy: {:<22} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2530 ), 2531 "| | Sell: {:<21} | {:<28} | | |\n".format( 2532 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2533 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2534 ), 2535 ]) 2536 2537 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2538 for key in opsKeys: 2539 if key == "rub": 2540 continue 2541 2542 info.extend([ 2543 "| | | {:<28} | | |\n".format( 2544 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2545 ), 2546 "| | | {:<28} | | |\n".format( 2547 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2548 ), 2549 ]) 2550 2551 info.append(splitLine1) 2552 2553 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2554 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2555 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2558 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2559 ) 2560 2561 # --- view "Payments" lines: 2562 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2563 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2564 2565 for key in paymentsKeys: 2566 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2567 2568 info.append(splitLine1) 2569 2570 # --- view "Commissions and taxes" lines: 2571 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2572 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2573 2574 for key in comKeys: 2575 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2576 2577 info.append(splitLine1) 2578 2579 info.extend([ 2580 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2581 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2582 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2583 ]) 2584 2585 else: 2586 info.append("Broker returned no operations during this period\n") 2587 2588 # --- view "Operations" section: 2589 for item in ops: 2590 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2591 continue 2592 2593 else: 2594 self.figi = item["figi"] if item["figi"] else "" 2595 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2596 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2597 2598 # group of deals during one day: 2599 if nextDay and item["date"].split("T")[0] != nextDay: 2600 info.append(splitLine2) 2601 nextDay = "" 2602 2603 else: 2604 nextDay = item["date"].split("T")[0] # saving current day for splitting 2605 2606 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2607 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2608 self.figi if self.figi else "—", 2609 instrument["ticker"] if instrument else "—", 2610 instrument["type"] if instrument else "—", 2611 item["quantity"] if int(item["quantity"]) > 0 else "—", 2612 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2613 TKS_OPERATION_STATES[item["state"]], 2614 TKS_OPERATION_TYPES[item["operationType"]], 2615 )) 2616 2617 infoText = "".join(info) 2618 2619 if show: 2620 uLogger.info(infoText) 2621 2622 if self.reportFile: 2623 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2624 fH.write(infoText) 2625 2626 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2627 2628 return ops, customStat 2629 2630 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2631 """ 2632 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2633 2634 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2635 Warning! Broker server used ISO UTC time by default. 2636 2637 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2638 Also, `historyFile` used to update history with `onlyMissing` parameter. 2639 2640 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2641 2642 :param start: see docstring in `GetDatesAsString()` method. 2643 :param end: see docstring in `GetDatesAsString()` method. 2644 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2645 `"hour"`, `"day"`. Default: `"hour"`. 2646 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2647 False by default. Warning! History appends only from last candle to current time 2648 with always update last candle! 2649 :param csvSep: separator if csv-file is used, `,` by default. 2650 :param show: if `True` then also prints Pandas DataFrame to the console. 2651 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2652 `["date", "time", "open", "high", "low", "close", "volume"]`. 2653 """ 2654 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2655 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2656 history = None # empty pandas object for history 2657 2658 if interval not in TKS_CANDLE_INTERVALS.keys(): 2659 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2660 raise Exception("Incorrect value") 2661 2662 if not (self.ticker or self.figi): 2663 uLogger.error("Ticker or FIGI must be defined!") 2664 raise Exception("Ticker or FIGI required") 2665 2666 if self.ticker and not self.figi: 2667 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2668 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2669 2670 if self.figi and not self.ticker: 2671 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2672 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2673 2674 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2675 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2676 if interval.lower() != "day": 2677 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2678 2679 delta = dtEnd - dtStart # current UTC time minus last time in file 2680 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2681 2682 # calculate history length in candles: 2683 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2684 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2685 length += 1 # to avoid fraction time 2686 2687 # calculate data blocks count: 2688 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2689 2690 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2691 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2692 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2693 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2694 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2695 2696 tempOld = None # pandas object for old history, if --only-missing key present 2697 lastTime = None # datetime object of last old candle in file 2698 2699 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2700 uLogger.debug("--only-missing key present, add only last missing candles...") 2701 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2702 2703 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2704 2705 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2706 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2707 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2708 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2709 2710 # get last datetime object from last string in file or minus 1 delta if file is empty: 2711 if len(tempOld) > 0: 2712 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2713 2714 else: 2715 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2716 2717 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2718 2719 responseJSONs = [] # raw history blocks of data 2720 2721 blockEnd = dtEnd 2722 for item in range(blocks): 2723 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2724 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2725 2726 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2727 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2728 )) 2729 2730 if blockStart == blockEnd: 2731 uLogger.debug("Skipped this zero-length block...") 2732 2733 else: 2734 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2735 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2736 self.body = str({ 2737 "figi": self.figi, 2738 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2739 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2740 "interval": TKS_CANDLE_INTERVALS[interval][0] 2741 }) 2742 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2743 2744 if "code" in responseJSON.keys(): 2745 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2746 2747 else: 2748 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2749 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2750 2751 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2752 2753 blockEnd = blockStart 2754 2755 printCount = len(responseJSONs) # candles to show in console 2756 if responseJSONs: 2757 tempHistory = pd.DataFrame( 2758 data={ 2759 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2761 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2762 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2763 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2764 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2765 "volume": [int(item["volume"]) for item in responseJSONs], 2766 }, 2767 index=range(len(responseJSONs)), 2768 columns=["date", "time", "open", "high", "low", "close", "volume"], 2769 ) 2770 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2771 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2772 2773 # append only newest candles to old history if --only-missing key present: 2774 if onlyMissing and tempOld is not None and lastTime is not None: 2775 index = 0 # find start index in tempHistory data: 2776 2777 for i, item in tempHistory.iterrows(): 2778 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2779 2780 if curTime == lastTime: 2781 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2782 index = i 2783 printCount = index + 1 2784 break 2785 2786 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2787 2788 else: 2789 history = tempHistory # if no `--only-missing` key then load full data from server 2790 2791 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2792 2793 if history is not None and not history.empty: 2794 if show: 2795 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2796 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2797 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2798 )) 2799 2800 else: 2801 uLogger.warning("Received an empty candles history!") 2802 2803 if self.historyFile is not None: 2804 if history is not None and not history.empty: 2805 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2806 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2807 2808 else: 2809 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2810 2811 else: 2812 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2813 2814 return history 2815 2816 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2817 """ 2818 Load candles history from csv-file and return Pandas DataFrame object. 2819 2820 See also: `History()` and `ShowHistoryChart()` methods. 2821 2822 :param filePath: path to csv-file to open. 2823 """ 2824 loadedHistory = None # init candles data object 2825 2826 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2827 2828 if os.path.exists(filePath): 2829 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2830 2831 tfStr = self.priceModel.FormattedDelta( 2832 self.priceModel.timeframe, 2833 "{days} days {hours}h {minutes}m {seconds}s", 2834 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2835 self.priceModel.timeframe, 2836 "{hours}h {minutes}m {seconds}s", 2837 ) 2838 2839 if loadedHistory is not None and not loadedHistory.empty: 2840 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2841 len(loadedHistory), 2842 tfStr, 2843 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2844 ) 2845 2846 else: 2847 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2848 2849 else: 2850 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2851 2852 return loadedHistory 2853 2854 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2855 """ 2856 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2857 2858 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2859 Default: `index.html` (both for interact and non-interact candlesticks chart). 2860 2861 See also: `History()` and `LoadHistory()` methods. 2862 2863 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2864 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2865 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2866 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2867 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2868 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2869 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2870 """ 2871 if isinstance(candles, str): 2872 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2873 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2874 2875 elif isinstance(candles, pd.DataFrame): 2876 self.priceModel.prices = candles # set candles chain from variable 2877 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2878 2879 if "datetime" not in candles.columns: 2880 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2881 2882 else: 2883 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2884 raise Exception("Incorrect value") 2885 2886 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2887 2888 if interact: 2889 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2890 2891 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2892 2893 else: 2894 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2895 2896 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2897 2898 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2899 2900 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2901 """ 2902 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2903 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2904 2905 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2906 2907 :param operation: string "Buy" or "Sell". 2908 :param lots: volume, integer count of lots >= 1. 2909 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2910 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2911 :param expDate: string "Undefined" by default or local date in future, 2912 it is a string with format `%Y-%m-%d %H:%M:%S`. 2913 :return: JSON with response from broker server. 2914 """ 2915 if self.accountId is None or not self.accountId: 2916 uLogger.error("Variable `accountId` must be defined for using this method!") 2917 raise Exception("Account ID required") 2918 2919 if operation is None or not operation or operation not in ("Buy", "Sell"): 2920 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2921 raise Exception("Incorrect value") 2922 2923 if lots is None or lots < 1: 2924 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2925 lots = 1 2926 2927 if tp is None or tp < 0: 2928 tp = 0 2929 2930 if sl is None or sl < 0: 2931 sl = 0 2932 2933 if expDate is None or not expDate: 2934 expDate = "Undefined" 2935 2936 if not (self.ticker or self.figi): 2937 uLogger.error("Ticker or FIGI must be defined!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2941 self.ticker = instrument["ticker"] 2942 self.figi = instrument["figi"] 2943 2944 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2945 2946 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2947 self.body = str({ 2948 "figi": self.figi, 2949 "quantity": str(lots), 2950 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2951 "accountId": str(self.accountId), 2952 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2953 }) 2954 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2955 2956 if "orderId" in response.keys(): 2957 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2958 operation, response["orderId"], 2959 self.ticker, self.figi, lots, 2960 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2961 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2962 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2963 )) 2964 2965 else: 2966 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2967 2968 if tp > 0: 2969 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2970 2971 if sl > 0: 2972 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2973 2974 return response 2975 2976 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2977 """ 2978 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2979 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2980 2981 See also: `Order()` and `Trade()` docstrings. 2982 2983 :param lots: volume, integer count of lots >= 1. 2984 :param tp: float > 0, take profit price of stop-order. 2985 :param sl: float > 0, stop loss price of stop-order. 2986 :param expDate: it's a local date in future. 2987 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2988 :return: JSON with response from broker server. 2989 """ 2990 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2991 2992 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` and `Trade()` docstrings. 2998 2999 :param lots: volume, integer count of lots >= 1. 3000 :param tp: float > 0, take profit price of stop-order. 3001 :param sl: float > 0, stop loss price of stop-order. 3002 :param expDate: it's a local date in the future. 3003 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3004 :return: JSON with response from broker server. 3005 """ 3006 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3007 3008 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3009 """ 3010 Close position of given instruments. 3011 3012 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3013 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3014 This avoids unnecessary downloading data from the server. 3015 """ 3016 if instruments is None or not instruments: 3017 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3018 raise Exception("Ticker or FIGI required") 3019 3020 if isinstance(instruments, str): 3021 instruments = [instruments] 3022 3023 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3024 if uniqueInstruments: 3025 if portfolio is None or not portfolio: 3026 portfolio = self.Overview(show=False) 3027 3028 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3029 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3030 3031 for self.figi in uniqueInstruments: 3032 if self.figi not in allOpened: 3033 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3034 continue 3035 3036 # search open trade info about instrument by ticker: 3037 instrument = {} 3038 for iType in TKS_INSTRUMENTS: 3039 if instrument: 3040 break 3041 3042 for item in portfolio["stat"][iType]: 3043 if item["figi"] == self.figi: 3044 instrument = item 3045 break 3046 3047 if instrument: 3048 self.ticker = instrument["ticker"] 3049 self.figi = instrument["figi"] 3050 3051 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3052 self.ticker, 3053 self.figi, 3054 int(instrument["volume"]), 3055 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3056 )) 3057 3058 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3059 3060 if tradeLots > 0: 3061 if instrument["blocked"] > 0: 3062 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3063 instrument["blocked"], 3064 self.ticker, 3065 tradeLots, 3066 )) 3067 3068 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3069 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3070 3071 else: 3072 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3073 3074 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3075 """ 3076 Close all positions of given instruments with defined type. 3077 3078 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3079 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3080 This avoids unnecessary downloading data from the server. 3081 """ 3082 if iType not in TKS_INSTRUMENTS: 3083 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3084 3085 else: 3086 if portfolio is None or not portfolio: 3087 portfolio = self.Overview(show=False) 3088 3089 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3090 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3091 3092 if tickers and portfolio: 3093 self.CloseTrades(tickers, portfolio) 3094 3095 else: 3096 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3097 3098 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3099 """ 3100 Universal method to create market or limit orders with all available parameters for current `accountId`. 3101 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3102 3103 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3104 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3105 3106 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3107 then broker immediately open market order as you can do simple --buy or --sell operations! 3108 3109 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3110 When current price will go up or down to target price value then broker opens a limit order. 3111 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3112 3113 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3114 3115 :param operation: string "Buy" or "Sell". 3116 :param orderType: string "Limit" or "Stop". 3117 :param lots: volume, integer count of lots >= 1. 3118 :param targetPrice: target price > 0. This is open trade price for limit order. 3119 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3120 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3121 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3122 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3123 Stop loss order always executed by market price. 3124 :param expDate: string "Undefined" by default or local date in future. 3125 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3126 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3127 A limit order has no expiration date, it lasts until the end of the trading day. 3128 :return: JSON with response from broker server. 3129 """ 3130 if self.accountId is None or not self.accountId: 3131 uLogger.error("Variable `accountId` must be defined for using this method!") 3132 raise Exception("Account ID required") 3133 3134 if operation is None or not operation or operation not in ("Buy", "Sell"): 3135 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3136 raise Exception("Incorrect value") 3137 3138 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3139 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3140 raise Exception("Incorrect value") 3141 3142 if lots is None or lots < 1: 3143 uLogger.error("You must define trade volume > 0: integer count of lots!") 3144 raise Exception("Incorrect value") 3145 3146 if targetPrice is None or targetPrice <= 0: 3147 uLogger.error("Target price for limit-order must be greater than 0!") 3148 raise Exception("Incorrect value") 3149 3150 if limitPrice is None or limitPrice <= 0: 3151 limitPrice = targetPrice 3152 3153 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3154 stopType = "Limit" 3155 3156 if expDate is None or not expDate: 3157 expDate = "Undefined" 3158 3159 if not (self.ticker or self.figi): 3160 uLogger.error("Tocker or FIGI must be defined!") 3161 raise Exception("Ticker or FIGI required") 3162 3163 response = {} 3164 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3165 self.ticker = instrument["ticker"] 3166 self.figi = instrument["figi"] 3167 3168 if orderType == "Limit": 3169 uLogger.debug( 3170 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3171 self.ticker, self.figi, 3172 operation, lots, targetPrice, instrument["currency"], 3173 )) 3174 3175 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3176 self.body = str({ 3177 "figi": self.figi, 3178 "quantity": str(lots), 3179 "price": FloatToNano(targetPrice), 3180 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3181 "accountId": str(self.accountId), 3182 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3183 }) 3184 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3185 3186 if "orderId" in response.keys(): 3187 uLogger.info( 3188 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3189 response["orderId"], 3190 self.ticker, self.figi, 3191 operation, lots, targetPrice, instrument["currency"], 3192 )) 3193 3194 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3195 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3196 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3197 targetPrice, instrument["currency"], 3198 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3199 )) 3200 3201 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3202 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3203 targetPrice, instrument["currency"], 3204 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3205 )) 3206 3207 else: 3208 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3209 3210 if orderType == "Stop": 3211 uLogger.debug( 3212 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3213 self.ticker, self.figi, 3214 operation, lots, 3215 targetPrice, instrument["currency"], 3216 limitPrice, instrument["currency"], 3217 stopType, expDate, 3218 )) 3219 3220 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3221 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3222 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3223 3224 body = { 3225 "figi": self.figi, 3226 "quantity": str(lots), 3227 "price": FloatToNano(limitPrice), 3228 "stopPrice": FloatToNano(targetPrice), 3229 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3230 "accountId": str(self.accountId), 3231 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3232 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3233 } 3234 3235 if expDateUTC: 3236 body["expireDate"] = expDateUTC 3237 3238 self.body = str(body) 3239 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3240 3241 if "stopOrderId" in response.keys(): 3242 uLogger.info( 3243 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3244 response["stopOrderId"], 3245 self.ticker, self.figi, 3246 operation, lots, 3247 targetPrice, instrument["currency"], 3248 limitPrice, instrument["currency"], 3249 TKS_STOP_ORDER_TYPES[stopOrderType], 3250 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3251 )) 3252 3253 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3254 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3255 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3256 targetPrice, instrument["currency"], 3257 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3258 )) 3259 3260 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3261 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3262 targetPrice, instrument["currency"], 3263 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3264 )) 3265 3266 else: 3267 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3268 3269 return response 3270 3271 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3272 """ 3273 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3274 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3275 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3276 See also: `Order()` docstring. 3277 3278 :param lots: volume, integer count of lots >= 1. 3279 :param targetPrice: target price > 0. This is open trade price for limit order. 3280 :return: JSON with response from broker server. 3281 """ 3282 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3283 3284 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3285 """ 3286 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3287 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3288 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3289 target price value then broker opens a limit order. See also: `Order()` docstring. 3290 3291 :param lots: volume, integer count of lots >= 1. 3292 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3293 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3294 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3295 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3296 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3297 :param expDate: string "Undefined" by default or local date in future. 3298 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3299 This date is converting to UTC format for server. 3300 :return: JSON with response from broker server. 3301 """ 3302 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3303 3304 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3305 """ 3306 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3307 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3308 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3309 See also: `Order()` docstring. 3310 3311 :param lots: volume, integer count of lots >= 1. 3312 :param targetPrice: target price > 0. This is open trade price for limit order. 3313 :return: JSON with response from broker server. 3314 """ 3315 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3316 3317 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3318 """ 3319 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3320 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3321 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3322 target price value then broker opens a limit order. See also: `Order()` docstring. 3323 3324 :param lots: volume, integer count of lots >= 1. 3325 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3326 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3327 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3328 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3329 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3330 :param expDate: string "Undefined" by default or local date in future. 3331 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3332 This date is converting to UTC format for server. 3333 :return: JSON with response from broker server. 3334 """ 3335 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3336 3337 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3338 """ 3339 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3340 3341 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3342 :param allOrdersIDs: pre-received lists of all active pending orders. 3343 This avoids unnecessary downloading data from the server. 3344 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3345 """ 3346 if self.accountId is None or not self.accountId: 3347 uLogger.error("Variable `accountId` must be defined for using this method!") 3348 raise Exception("Account ID required") 3349 3350 if orderIDs: 3351 if allOrdersIDs is None or not allOrdersIDs: 3352 rawOrders = self.RequestPendingOrders() 3353 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3354 3355 if allStopOrdersIDs is None or not allStopOrdersIDs: 3356 rawStopOrders = self.RequestStopOrders() 3357 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3358 3359 for orderID in orderIDs: 3360 idInPendingOrders = orderID in allOrdersIDs 3361 idInStopOrders = orderID in allStopOrdersIDs 3362 3363 if not (idInPendingOrders or idInStopOrders): 3364 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3365 continue 3366 3367 else: 3368 if idInPendingOrders: 3369 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3370 3371 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3372 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3373 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3374 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3375 3376 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3377 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3378 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3379 3380 else: 3381 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3382 3383 elif idInStopOrders: 3384 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3385 3386 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3387 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3388 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3389 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3390 3391 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3392 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3393 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3394 3395 else: 3396 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3397 3398 else: 3399 continue 3400 3401 def CloseAllOrders(self) -> None: 3402 """ 3403 Gets a list of open pending and stop orders and cancel it all. 3404 """ 3405 rawOrders = self.RequestPendingOrders() 3406 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3407 lenOrders = len(allOrdersIDs) 3408 3409 rawStopOrders = self.RequestStopOrders() 3410 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3411 lenSOrders = len(allStopOrdersIDs) 3412 3413 if lenOrders > 0 or lenSOrders > 0: 3414 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3415 3416 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3417 3418 else: 3419 uLogger.info("Orders not found, nothing to cancel.") 3420 3421 def CloseAll(self, *args) -> None: 3422 """ 3423 Close all available (not blocked) opened trades and orders. 3424 3425 Also, you can select one or more keywords case-insensitive: 3426 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3427 3428 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3429 """ 3430 overview = self.Overview(show=False) # get all open trades info 3431 3432 if len(args) == 0: 3433 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3434 self.CloseAllOrders() # close all pending and stop orders 3435 3436 for iType in TKS_INSTRUMENTS: 3437 if iType != "Currencies": 3438 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3439 3440 else: 3441 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3442 lowerArgs = [x.lower() for x in args] 3443 3444 if "orders" in lowerArgs: 3445 self.CloseAllOrders() # close all pending and stop orders 3446 3447 for iType in TKS_INSTRUMENTS: 3448 if iType.lower() in lowerArgs and iType != "Currencies": 3449 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3450 3451 @staticmethod 3452 def ParseOrderParameters(operation, **inputParameters): 3453 """ 3454 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3455 3456 :param operation: string "Buy" or "Sell". 3457 :param inputParameters: this is dict of strings that looks like this 3458 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3459 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3460 "prices" key: one or more prices to open limit-orders 3461 Counts of values in lots and prices lists must be equals! 3462 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3463 """ 3464 # TODO: update order grid work with api v2 3465 pass 3466 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3467 # 3468 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3469 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3470 # raise Exception("Incorrect value") 3471 # 3472 # if "l" in inputParameters.keys(): 3473 # inputParameters["lots"] = inputParameters.pop("l") 3474 # 3475 # if "p" in inputParameters.keys(): 3476 # inputParameters["prices"] = inputParameters.pop("p") 3477 # 3478 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3479 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3480 # raise Exception("Incorrect value") 3481 # 3482 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3483 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3484 # 3485 # if len(lots) != len(prices): 3486 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3487 # raise Exception("Incorrect value") 3488 # 3489 # uLogger.debug("Extracted parameters for orders:") 3490 # uLogger.debug("lots = {}".format(lots)) 3491 # uLogger.debug("prices = {}".format(prices)) 3492 # 3493 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3494 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3495 # uLogger.debug("Order parameters: {}".format(result)) 3496 # 3497 # return result 3498 3499 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3500 """ 3501 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3502 3503 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3504 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3505 """ 3506 result = False 3507 msg = "Instrument not defined!" 3508 3509 if portfolio is None or not portfolio: 3510 portfolio = self.Overview(show=False) 3511 3512 if self.ticker: 3513 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3514 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3515 3516 for iType in TKS_INSTRUMENTS: 3517 for instrument in portfolio["stat"][iType]: 3518 if instrument["ticker"] == self.ticker: 3519 result = True 3520 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3521 break 3522 3523 elif self.figi: 3524 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3525 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3526 3527 for iType in TKS_INSTRUMENTS: 3528 for instrument in portfolio["stat"][iType]: 3529 if instrument["figi"] == self.figi: 3530 result = True 3531 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3532 break 3533 3534 else: 3535 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3536 3537 uLogger.debug(msg) 3538 3539 return result 3540 3541 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3542 """ 3543 Returns instrument is in the user's portfolio if it presents there. 3544 Instrument must be defined by `ticker` (highly priority) or `figi`. 3545 3546 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3547 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3548 """ 3549 result = None 3550 msg = "Instrument not defined!" 3551 3552 if portfolio is None or not portfolio: 3553 portfolio = self.Overview(show=False) 3554 3555 if self.ticker: 3556 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3557 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3558 3559 for iType in TKS_INSTRUMENTS: 3560 for instrument in portfolio["stat"][iType]: 3561 if instrument["ticker"] == self.ticker: 3562 result = instrument 3563 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3564 break 3565 3566 elif self.figi: 3567 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3568 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3569 3570 for iType in TKS_INSTRUMENTS: 3571 for instrument in portfolio["stat"][iType]: 3572 if instrument["figi"] == self.figi: 3573 result = instrument 3574 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3575 break 3576 3577 else: 3578 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3579 3580 uLogger.debug(msg) 3581 3582 return result 3583 3584 def RequestLimits(self) -> dict: 3585 """ 3586 Method for obtaining the available funds for withdrawal for current `accountId`. 3587 3588 See also: 3589 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3590 - `OverviewLimits()` method 3591 3592 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3593 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3594 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3595 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3596 """ 3597 if self.accountId is None or not self.accountId: 3598 uLogger.error("Variable `accountId` must be defined for using this method!") 3599 raise Exception("Account ID required") 3600 3601 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3602 3603 self.body = str({"accountId": self.accountId}) 3604 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3605 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3606 3607 uLogger.debug("Records about available funds for withdrawal successfully received") 3608 3609 return rawLimits 3610 3611 def OverviewLimits(self, show: bool = False) -> dict: 3612 """ 3613 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3614 3615 See also: `RequestLimits()`. 3616 3617 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3618 :return: dict with raw parsed data from server and some calculated statistics about it. 3619 """ 3620 if self.accountId is None or not self.accountId: 3621 uLogger.error("Variable `accountId` must be defined for using this method!") 3622 raise Exception("Account ID required") 3623 3624 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3625 3626 view = { 3627 "rawLimits": rawLimits, 3628 "limits": { # parsed data for every currency: 3629 "money": { # this is an array of portfolio currency positions 3630 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3631 }, 3632 "blocked": { # this is an array of blocked currency 3633 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3634 }, 3635 "blockedGuarantee": { # this is locked money under collateral for futures 3636 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3637 }, 3638 }, 3639 } 3640 3641 # --- Prepare text table with limits in human-readable format: 3642 if show: 3643 info = [ 3644 "# Withdrawal limits\n\n", 3645 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3646 "* **Account ID:** [{}]\n".format(self.accountId), 3647 ] 3648 3649 if view["limits"]["money"]: 3650 info.extend([ 3651 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3652 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3653 ]) 3654 3655 else: 3656 info.append("\nNo withdrawal limits\n") 3657 3658 for curr in view["limits"]["money"].keys(): 3659 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3660 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3661 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3662 3663 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3664 "[{}]".format(curr), 3665 "{:.2f}".format(view["limits"]["money"][curr]), 3666 "{:.2f}".format(availableMoney), 3667 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3668 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3669 ) 3670 3671 if curr == "rub": 3672 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3673 3674 else: 3675 info.append(infoStr) 3676 3677 infoText = "".join(info) 3678 3679 uLogger.info(infoText) 3680 3681 if self.withdrawalLimitsFile: 3682 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3683 fH.write(infoText) 3684 3685 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3686 3687 return view 3688 3689 def RequestAccounts(self) -> dict: 3690 """ 3691 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3692 3693 See also: 3694 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3695 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3696 - `OverviewUserInfo()` method 3697 3698 :return: dict with raw data from server that contains accounts info. Example of dict: 3699 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3700 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3701 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3702 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3703 """ 3704 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3705 3706 self.body = str({}) 3707 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3708 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3709 3710 uLogger.debug("Records about available accounts successfully received") 3711 3712 return rawAccounts 3713 3714 def RequestUserInfo(self) -> dict: 3715 """ 3716 Method for requesting common user's information. 3717 3718 See also: 3719 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3720 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3721 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3722 - `OverviewUserInfo()` method 3723 3724 :return: dict with raw data from server that contains user's information. Example of dict: 3725 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3726 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3727 """ 3728 uLogger.debug("Requesting common user's information. Wait, please...") 3729 3730 self.body = str({}) 3731 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3732 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3733 3734 uLogger.debug("Records about current user successfully received") 3735 3736 return rawUserInfo 3737 3738 def RequestMarginStatus(self, accountId: str = None) -> dict: 3739 """ 3740 Method for requesting margin calculation for defined account ID. 3741 3742 See also: 3743 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3744 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3745 - `OverviewUserInfo()` method 3746 3747 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3748 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3749 Example of responses: 3750 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3751 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3752 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3753 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3754 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3755 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3756 """ 3757 if accountId is None or not accountId: 3758 if self.accountId is None or not self.accountId: 3759 uLogger.error("Variable `accountId` must be defined for using this method!") 3760 raise Exception("Account ID required") 3761 3762 else: 3763 accountId = self.accountId # use `self.accountId` (main ID) by default 3764 3765 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3766 3767 self.body = str({"accountId": accountId}) 3768 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3769 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3770 3771 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3772 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3773 rawMargin = {} 3774 3775 else: 3776 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3777 3778 return rawMargin 3779 3780 def RequestTariffLimits(self) -> dict: 3781 """ 3782 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3783 3784 See also: 3785 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3786 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3787 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3788 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3789 - `OverviewUserInfo()` method 3790 3791 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3792 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3793 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3794 """ 3795 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3796 3797 self.body = str({}) 3798 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3799 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3800 3801 uLogger.debug("Records with limits of current tariff successfully received") 3802 3803 return rawTariffLimits 3804 3805 def RequestBondCoupons(self, iJSON: dict) -> dict: 3806 """ 3807 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3808 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3809 All dates are in UTC timezone. 3810 3811 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3812 Documentation: 3813 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3814 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3815 3816 See also: `ExtendBondsData()`. 3817 3818 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3819 If raw iJSON is not data of bond then server returns an error [400] with message: 3820 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3821 :return: dictionary with bond payment calendar. Response example 3822 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3823 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3824 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3825 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3826 """ 3827 if iJSON["figi"] is None or not iJSON["figi"]: 3828 uLogger.error("FIGI must be defined for using this method!") 3829 raise Exception("FIGI required") 3830 3831 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3832 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3833 3834 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3835 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3836 self.figi, 3837 startDate, 3838 endDate, 3839 )) 3840 3841 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3842 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3843 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3844 3845 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3846 uLogger.warning("Instrument type is not bond!") 3847 3848 else: 3849 uLogger.debug("Records about bond payment calendar successfully received") 3850 3851 return calendar 3852 3853 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3854 """ 3855 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3856 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3857 coupon yields, current yields and some statistics etc. 3858 3859 WARNING! This is too long operation if a lot of bonds requested from broker server. 3860 3861 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3862 3863 :param instruments: list of strings with tickers or FIGIs. 3864 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3865 for further used by data scientists or stock analytics. 3866 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3867 In XLSX-file and Pandas DataFrame fields mean: 3868 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3869 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3870 """ 3871 if instruments is None or not instruments: 3872 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3873 raise Exception("Ticker or FIGI required") 3874 3875 if isinstance(instruments, str): 3876 instruments = [instruments] 3877 3878 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3879 3880 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3881 3882 iCount = len(uniqueInstruments) 3883 tooLong = iCount >= 20 3884 if tooLong: 3885 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3886 3887 bonds = None 3888 for i, self.figi in enumerate(uniqueInstruments): 3889 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3890 3891 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3892 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3893 rawBond = self.SearchByFIGI(requestPrice=True) 3894 3895 # Widen raw data with UTC current time (iData["actualDateTime"]): 3896 actualDate = datetime.now(tzutc()) 3897 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3898 3899 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3900 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3901 3902 # Replace some values with human-readable: 3903 iData["nominalCurrency"] = iData["nominal"]["currency"] 3904 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3905 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3906 iData["aciCurrency"] = iData["aciValue"]["currency"] 3907 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3908 iData["issueSize"] = int(iData["issueSize"]) 3909 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3910 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3911 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3912 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3913 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3914 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3915 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3916 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3917 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3918 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3919 3920 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3921 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3922 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3923 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3924 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3925 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3926 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3927 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3928 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3929 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3930 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3931 3932 # Widen raw data with calendar data from `rawCalendar` values: 3933 calendarData = [] 3934 for item in iData["rawCalendar"]["events"]: 3935 calendarData.append({ 3936 "couponDate": item["couponDate"], 3937 "couponNumber": int(item["couponNumber"]), 3938 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3939 "payCurrency": item["payOneBond"]["currency"], 3940 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3941 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3942 "couponStartDate": item["couponStartDate"], 3943 "couponEndDate": item["couponEndDate"], 3944 "couponPeriod": item["couponPeriod"], 3945 }) 3946 3947 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3948 if "maturityDate" not in iData.keys(): 3949 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3950 3951 # Widen raw data with Coupon Rate. 3952 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3953 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3954 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3955 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3956 3957 # Widen raw data with Yield to Maturity (YTM) on current date. 3958 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3959 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3960 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3961 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3962 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3963 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3964 3965 iData["calendar"] = calendarData # adds calendar at the end 3966 3967 # Remove not used data: 3968 iData.pop("uid") 3969 iData.pop("positionUid") 3970 iData.pop("currentPrice") 3971 iData.pop("rawCalendar") 3972 3973 colNames = list(iData.keys()) 3974 if bonds is None: 3975 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3976 3977 else: 3978 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3979 3980 else: 3981 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3982 3983 processed = round(100 * (i + 1) / iCount, 1) 3984 if tooLong and processed % 5 == 0: 3985 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3986 3987 else: 3988 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3989 3990 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3991 3992 # Saving bonds from Pandas DataFrame to XLSX sheet: 3993 if xlsx and self.bondsXLSXFile: 3994 with pd.ExcelWriter( 3995 path=self.bondsXLSXFile, 3996 date_format=TKS_DATE_FORMAT, 3997 datetime_format=TKS_DATE_TIME_FORMAT, 3998 mode="w", 3999 ) as writer: 4000 bonds.to_excel( 4001 writer, 4002 sheet_name="Extended bonds data", 4003 index=True, 4004 encoding="UTF-8", 4005 freeze_panes=(1, 1), 4006 ) # saving as XLSX-file with freeze first row and column as headers 4007 4008 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4009 4010 return bonds 4011 4012 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4013 """ 4014 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4015 4016 WARNING! This is too long operation if a lot of bonds requested from broker server. 4017 4018 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4019 4020 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4021 extended information about bonds: main info, current prices, bond payment calendar, 4022 coupon yields, current yields and some statistics etc. 4023 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4024 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4025 for further used by data scientists or stock analytics. 4026 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4027 """ 4028 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4029 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4030 4031 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4032 4033 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4034 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4035 calendar = None 4036 for bond in extBonds.iterrows(): 4037 for item in bond[1]["calendar"]: 4038 cData = { 4039 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4040 "couponDate": item["couponDate"], 4041 "figi": bond[1]["figi"], 4042 "ticker": bond[1]["ticker"], 4043 "name": bond[1]["name"], 4044 "couponNumber": item["couponNumber"], 4045 "payOneBond": item["payOneBond"], 4046 "payCurrency": item["payCurrency"], 4047 "couponType": item["couponType"], 4048 "couponPeriod": item["couponPeriod"], 4049 "fixDate": item["fixDate"], 4050 "couponStartDate": item["couponStartDate"], 4051 "couponEndDate": item["couponEndDate"], 4052 } 4053 4054 if calendar is None: 4055 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4056 4057 else: 4058 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4059 4060 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4061 4062 # Saving calendar from Pandas DataFrame to XLSX sheet: 4063 if xlsx: 4064 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4065 4066 with pd.ExcelWriter( 4067 path=xlsxCalendarFile, 4068 date_format=TKS_DATE_FORMAT, 4069 datetime_format=TKS_DATE_TIME_FORMAT, 4070 mode="w", 4071 ) as writer: 4072 humanReadable = calendar.copy(deep=True) 4073 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4074 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4075 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4076 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4077 humanReadable.columns = colNames # human-readable column names 4078 4079 humanReadable.to_excel( 4080 writer, 4081 sheet_name="Bond payments calendar", 4082 index=False, 4083 encoding="UTF-8", 4084 freeze_panes=(1, 2), 4085 ) # saving as XLSX-file with freeze first row and column as headers 4086 4087 del humanReadable # release df in memory 4088 4089 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4090 4091 return calendar 4092 4093 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4094 """ 4095 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4096 Also, creates Markdown file with calendar data, `calendar.md` by default. 4097 4098 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4099 4100 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4101 extended information about bonds: main info, current prices, bond payment calendar, 4102 coupon yields, current yields and some statistics etc. 4103 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4104 :param show: if `True` then also printing bonds payment calendar to the console, 4105 otherwise save to file `calendarFile` only. `False` by default. 4106 :return: multilines text in Markdown format with bonds payment calendar as a table. 4107 """ 4108 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4109 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4110 4111 infoText = "# Bond payments calendar\n\n" 4112 4113 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4114 4115 if not calendar.empty: 4116 splitLine = "| | | | | | | | | |\n" 4117 4118 info = [ 4119 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4120 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4121 ] 4122 4123 newMonth = False 4124 notOneBond = calendar["figi"].nunique() > 1 4125 for i, bond in enumerate(calendar.iterrows()): 4126 if newMonth and notOneBond: 4127 info.append(splitLine) 4128 4129 info.append( 4130 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4131 " √" if bond[1]["paid"] else " —", 4132 bond[1]["couponDate"].split("T")[0], 4133 bond[1]["figi"], 4134 bond[1]["ticker"], 4135 bond[1]["couponNumber"], 4136 "{} {}".format( 4137 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4138 bond[1]["payCurrency"], 4139 ), 4140 bond[1]["couponType"], 4141 bond[1]["couponPeriod"], 4142 bond[1]["fixDate"].split("T")[0], 4143 ) 4144 ) 4145 4146 if i < len(calendar.values) - 1: 4147 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4148 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4149 newMonth = False if curDate.month == nextDate.month else True 4150 4151 else: 4152 newMonth = False 4153 4154 infoText += "".join(info) 4155 4156 if show: 4157 uLogger.info("{}".format(infoText)) 4158 4159 if self.calendarFile is not None: 4160 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4161 fH.write(infoText) 4162 4163 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4164 4165 else: 4166 infoText += "No data\n" 4167 4168 return infoText 4169 4170 def OverviewAccounts(self, show: bool = False) -> dict: 4171 """ 4172 Method for parsing and show simple table with all available user accounts. 4173 4174 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4175 4176 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4177 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4178 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4179 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4180 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4181 "closed": "—", "access": "Full access" }, ...}}` 4182 """ 4183 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4184 4185 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4186 accounts = { 4187 item["id"]: { 4188 "type": TKS_ACCOUNT_TYPES[item["type"]], 4189 "name": item["name"], 4190 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4191 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4192 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4193 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4194 } for item in rawAccounts["accounts"] 4195 } 4196 4197 # Raw and parsed data with some fields replaced in "stat" section: 4198 view = { 4199 "rawAccounts": rawAccounts, 4200 "stat": accounts, 4201 } 4202 4203 # --- Prepare simple text table with only accounts data in human-readable format: 4204 if show: 4205 info = [ 4206 "# User accounts\n\n", 4207 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4208 "| Account ID | Type | Status | Name |\n", 4209 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4210 ] 4211 4212 for account in view["stat"].keys(): 4213 info.extend([ 4214 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4215 account, 4216 view["stat"][account]["type"], 4217 view["stat"][account]["status"], 4218 view["stat"][account]["name"], 4219 ) 4220 ]) 4221 4222 infoText = "".join(info) 4223 4224 uLogger.info(infoText) 4225 4226 if self.userAccountsFile: 4227 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4228 fH.write(infoText) 4229 4230 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4231 4232 return view 4233 4234 def OverviewUserInfo(self, show: bool = False) -> dict: 4235 """ 4236 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4237 4238 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4239 4240 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4241 :return: dict with raw parsed data from server and some calculated statistics about it. 4242 """ 4243 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4244 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4245 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4246 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4247 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4248 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4249 4250 # This is dict with parsed common user data: 4251 userInfo = { 4252 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4253 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4254 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4255 "tariff": rawUserInfo["tariff"], 4256 } 4257 4258 # This is an array of dict with parsed margin statuses for every account IDs: 4259 margins = {} 4260 for accountId in accounts.keys(): 4261 if rawMargins[accountId]: 4262 margins[accountId] = { 4263 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4264 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4265 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4266 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4267 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4268 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4269 } 4270 4271 else: 4272 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4273 4274 unary = {} # unary-connection limits 4275 for item in rawTariffLimits["unaryLimits"]: 4276 if item["limitPerMinute"] in unary.keys(): 4277 unary[item["limitPerMinute"]].extend(item["methods"]) 4278 4279 else: 4280 unary[item["limitPerMinute"]] = item["methods"] 4281 4282 stream = {} # stream-connection limits 4283 for item in rawTariffLimits["streamLimits"]: 4284 if item["limit"] in stream.keys(): 4285 stream[item["limit"]].extend(item["streams"]) 4286 4287 else: 4288 stream[item["limit"]] = item["streams"] 4289 4290 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4291 limits = { 4292 "unary": unary, 4293 "stream": stream, 4294 } 4295 4296 # Raw and parsed data as an output result: 4297 view = { 4298 "rawUserInfo": rawUserInfo, 4299 "rawAccounts": rawAccounts, 4300 "rawMargins": rawMargins, 4301 "rawTariffLimits": rawTariffLimits, 4302 "stat": { 4303 "userInfo": userInfo, 4304 "accounts": accounts, 4305 "margins": margins, 4306 "limits": limits, 4307 }, 4308 } 4309 4310 # --- Prepare text table with user information in human-readable format: 4311 if show: 4312 info = [ 4313 "# Full user information\n\n", 4314 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4315 "## Common information\n\n", 4316 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4317 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4318 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4319 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4320 "\n## User accounts\n\n", 4321 ] 4322 4323 for account in view["stat"]["accounts"].keys(): 4324 info.extend([ 4325 "### ID: [{}]\n\n".format(account), 4326 "| Parameters | Values |\n", 4327 "|----------------------|--------------------------------------------------------------|\n", 4328 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4329 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4330 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4331 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4332 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4333 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4334 ]) 4335 4336 if margins[account]: 4337 info.extend([ 4338 "| Margin status: | Enabled |\n", 4339 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4340 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4341 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4342 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4343 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4344 ]) 4345 4346 else: 4347 info.append("| Margin status: | Disabled |\n\n") 4348 4349 info.extend([ 4350 "\n## Current user tariff limits\n", 4351 "\nSee also:\n", 4352 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4353 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4354 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4355 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4356 "\n### Unary limits\n", 4357 ]) 4358 4359 if unary: 4360 for key, values in sorted(unary.items()): 4361 info.append("\n* Max requests per minute: {}\n".format(key)) 4362 4363 for value in values: 4364 info.append(" - {}\n".format(value)) 4365 4366 else: 4367 info.append("\nNot available\n") 4368 4369 info.append("\n### Stream limits\n") 4370 4371 if stream: 4372 for key, values in sorted(stream.items()): 4373 info.append("\n* Max stream connections: {}\n".format(key)) 4374 4375 for value in values: 4376 info.append(" - {}\n".format(value)) 4377 4378 else: 4379 info.append("\nNot available\n") 4380 4381 infoText = "".join(info) 4382 4383 uLogger.info(infoText) 4384 4385 if self.userInfoFile: 4386 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4387 fH.write(infoText) 4388 4389 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4390 4391 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
- debug: if
Truethen print more debug information, e.g. request and response parameters, headers etc.
Returns
response JSON (dictionary) from broker.
591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON
Search and return raw broker's information about instrument by its ticker.
ticker must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON
Search and return raw broker's information about instrument by its FIGI.
figi must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1107 `{"buy": [{"price": 1243.8, "quantity": 193}, 1108 {"price": 1244.0, "quantity": 168}, 1109 {"price": 1244.8, "quantity": 5}, 1110 {"price": 1245.0, "quantity": 61}, 1111 {"price": 1245.4, "quantity": 60}], 1112 "sell": [{"price": 1243.6, "quantity": 8}, 1113 {"price": 1242.6, "quantity": 10}, 1114 {"price": 1242.4, "quantity": 18}, 1115 {"price": 1242.2, "quantity": 50}, 1116 {"price": 1242.0, "quantity": 113}], 1117 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1118 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1119 - sell: list of dicts with Buyers prices, 1120 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1121 - quantity: volume value by current price in lots, 1122 - limitUp: current trade session limit price, maximum, 1123 - limitDown: current trade session limit price, minimum, 1124 - lastPrice: last deal price of the instrument, 1125 - closePrice: previous trade session close price of the instrument. 1126 1127 See also: `SearchByTicker()` and `SearchByFIGI()`. 1128 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1129 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1130 1131 :param show: if `True` then print DOM to log and console. 1132 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1133 If an error occurred then returns an empty record: 1134 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1135 """ 1136 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1137 1138 if self.depth < 1: 1139 uLogger.error("Depth of Market (DOM) must be >=1!") 1140 raise Exception("Incorrect value") 1141 1142 if not (self.ticker or self.figi): 1143 uLogger.error("self.ticker or self.figi variables must be defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 if self.ticker and not self.figi: 1147 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1148 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1149 1150 if not self.ticker and self.figi: 1151 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1153 1154 if not self.figi: 1155 uLogger.error("FIGI is not defined!") 1156 raise Exception("Ticker or FIGI required") 1157 1158 else: 1159 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1160 1161 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1162 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1163 self.body = str({"figi": self.figi, "depth": self.depth}) 1164 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1165 1166 if pricesResponse: 1167 # list of dicts with sellers orders: 1168 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1169 1170 # list of dicts with buyers orders: 1171 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1172 1173 # max price of instrument at this time: 1174 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1175 1176 # min price of instrument at this time: 1177 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1178 1179 # last price of deal with instrument: 1180 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1181 1182 # last close price of instrument: 1183 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1184 1185 else: 1186 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1187 uLogger.debug("Server response: {}".format(pricesResponse)) 1188 1189 if show: 1190 if prices["buy"] or prices["sell"]: 1191 info = [ 1192 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1193 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1194 self.ticker, 1195 self.figi, 1196 self.depth, 1197 ), 1198 "-" * 60, "\n", 1199 " Orders of Buyers | Orders of Sellers\n", 1200 "-" * 60, "\n", 1201 " Sell prices (volumes) | Buy prices (volumes)\n", 1202 "-" * 60, "\n", 1203 ] 1204 1205 if not prices["buy"]: 1206 info.append(" | No orders!\n") 1207 sumBuy = 0 1208 1209 else: 1210 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1211 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1212 for item in maxMinSorted: 1213 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1214 1215 if not prices["sell"]: 1216 info.append("No orders! |\n") 1217 sumSell = 0 1218 1219 else: 1220 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1221 for item in prices["sell"]: 1222 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1223 1224 info.extend([ 1225 "-" * 60, "\n", 1226 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1227 "-" * 60, "\n", 1228 ]) 1229 1230 infoText = "".join(info) 1231 1232 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1233 1234 else: 1235 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1236 1237 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1239 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1240 """ 1241 This method get and show information about all available broker instruments for current user account. 1242 If `instrumentsFile` string is not empty then also save information to this file. 1243 1244 :param show: if `True` then print results to console, if `False` - print only to file. 1245 :return: multi-lines string with all available broker instruments 1246 """ 1247 if not self.iList: 1248 self.iList = self.Listing() 1249 1250 info = [ 1251 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1252 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1253 ] 1254 1255 # add instruments count by type: 1256 for iType in self.iList.keys(): 1257 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1258 1259 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1260 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1261 1262 # generating info tables with all instruments by type: 1263 for iType in self.iList.keys(): 1264 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1265 1266 for instrument in self.iList[iType].keys(): 1267 iName = self.iList[iType][instrument]["name"] # instrument's name 1268 if len(iName) > 57: 1269 iName = "{}...".format(iName[:54]) # right trim for a long string 1270 1271 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1272 self.iList[iType][instrument]["ticker"], 1273 iName, 1274 self.iList[iType][instrument]["figi"], 1275 self.iList[iType][instrument]["currency"], 1276 self.iList[iType][instrument]["lot"], 1277 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1278 )) 1279 1280 infoText = "".join(info) 1281 1282 if show: 1283 uLogger.info(infoText) 1284 1285 if self.instrumentsFile: 1286 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1287 fH.write(infoText) 1288 1289 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1290 1291 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1293 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` - return list of result only. 1300 :return: list of dictionaries with all found instruments. 1301 """ 1302 if not self.iList: 1303 self.iList = self.Listing() 1304 1305 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1306 compiledPattern = re.compile(pattern, re.IGNORECASE) 1307 1308 for iType in self.iList: 1309 for instrument in self.iList[iType].values(): 1310 searchResult = compiledPattern.search(" ".join( 1311 [instrument["ticker"], instrument["figi"], instrument["name"]] 1312 )) 1313 1314 if searchResult: 1315 searchResults[iType][instrument["ticker"]] = instrument 1316 1317 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1318 info = [ 1319 "# Search results\n\n", 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile: 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1372 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1373 """ 1374 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1375 1376 :param instruments: list of strings with tickers or FIGIs. 1377 :return: list with unique instrument FIGIs only. 1378 """ 1379 requestedInstruments = [] 1380 for iName in instruments: 1381 if iName not in self.aliases.keys(): 1382 if iName not in requestedInstruments: 1383 requestedInstruments.append(iName) 1384 1385 else: 1386 if iName not in requestedInstruments: 1387 if self.aliases[iName] not in requestedInstruments: 1388 requestedInstruments.append(self.aliases[iName]) 1389 1390 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1391 1392 onlyUniqueFIGIs = [] 1393 for iName in requestedInstruments: 1394 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1395 continue 1396 1397 self.ticker = iName 1398 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1399 1400 if not iData: 1401 self.ticker = "" 1402 self.figi = iName 1403 1404 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1405 1406 if not iData: 1407 self.figi = "" 1408 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1409 1410 if iData and iData["figi"] not in onlyUniqueFIGIs: 1411 onlyUniqueFIGIs.append(iData["figi"]) 1412 1413 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1414 1415 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1417 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1418 """ 1419 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 If `pricesFile` string is not empty then also save information to this file. 1422 1423 :param instruments: list of strings with tickers or FIGIs. 1424 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1425 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1427 """ 1428 if instruments is None or not instruments: 1429 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1430 raise Exception("Ticker or FIGI required") 1431 1432 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1433 1434 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1435 1436 iList = [] # trying to get info and current prices about all unique instruments: 1437 for self.figi in onlyUniqueFIGIs: 1438 iData = self.SearchByFIGI(requestPrice=True) 1439 iList.append(iData) 1440 1441 self.ShowListOfPrices(iList, show) 1442 1443 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1445 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1446 """ 1447 Show table contains current prices of given instruments. 1448 1449 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1451 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1452 :return: multilines text in Markdown format as a table contains current prices. 1453 """ 1454 infoText = "" 1455 1456 if show or self.pricesFile: 1457 info = [ 1458 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1459 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1460 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1461 ] 1462 1463 for item in iList: 1464 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1465 item["ticker"], 1466 item["figi"], 1467 item["type"], 1468 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1469 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1470 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1471 "{} / {}".format( 1472 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1473 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1474 ), 1475 "{} / {}".format( 1476 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1477 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1478 ), 1479 item["currency"], 1480 )) 1481 1482 infoText = "".join(info) 1483 1484 if show: 1485 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1486 1487 if self.pricesFile: 1488 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1489 fH.write(infoText) 1490 1491 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1492 1493 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1495 def RequestTradingStatus(self) -> dict: 1496 """ 1497 Requesting trading status for the instrument defined by `figi` variable. 1498 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1499 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1500 1501 :return: dictionary with trading status attributes. Response example: 1502 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1503 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1504 """ 1505 if self.figi is None or not self.figi: 1506 uLogger.error("Variable `figi` must be defined for using this method!") 1507 raise Exception("FIGI required") 1508 1509 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1510 1511 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1512 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1513 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1514 1515 uLogger.debug("Records about current trading status successfully received") 1516 1517 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1519 def RequestPortfolio(self) -> dict: 1520 """ 1521 Requesting actual user's portfolio for current `accountId`. 1522 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1524 1525 :return: dictionary with user's portfolio. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1535 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1536 1537 uLogger.debug("Records about user's portfolio successfully received") 1538 1539 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1541 def RequestPositions(self) -> dict: 1542 """ 1543 Requesting open positions by currencies and instruments for current `accountId`. 1544 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1545 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1546 1547 :return: dictionary with open positions by instruments. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1557 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1558 1559 uLogger.debug("Records about current open positions successfully received") 1560 1561 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1563 def RequestPendingOrders(self) -> list: 1564 """ 1565 Requesting current actual pending orders for current `accountId`. 1566 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1580 1581 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1582 1583 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1602 1603 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1604 1605 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1607 def Overview(self, show: bool = False, details: str = "full") -> dict: 1608 """ 1609 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1610 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1611 are defined then also save information to file. 1612 1613 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1614 many requests about the state of the portfolio, and then, based on the received data, a large number 1615 of calculation and statistics are collected. 1616 1617 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1618 :param details: how detailed should the information be? You should specify one of strings: 1619 `full` - shows full available information about portfolio status (by default), 1620 `positions` - shows only open positions, 1621 `digest` - show a short digest of the portfolio status, 1622 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1623 `orders` - shows only sections of open limits and stop orders. 1624 :return: dictionary with client's raw portfolio and some statistics. 1625 """ 1626 if self.accountId is None or not self.accountId: 1627 uLogger.error("Variable `accountId` must be defined for using this method!") 1628 raise Exception("Account ID required") 1629 1630 view = { 1631 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1632 "headers": {}, # list of dictionaries, response headers without "positions" section 1633 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1634 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1635 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1636 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1637 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1638 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1639 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1640 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1641 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1642 }, 1643 "stat": { # --- some statistics calculated using "raw" sections: 1644 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1645 "availableRUB": 0., # available rubles (without other currencies) 1646 "blockedRUB": 0., # blocked sum in Russian Rouble 1647 "totalChangesRUB": 0., # changes for all open trades in RUB 1648 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1649 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1650 "sharesCostRUB": 0., # costs of all shares in RUB 1651 "bondsCostRUB": 0., # costs of all bonds in RUB 1652 "etfsCostRUB": 0., # costs of all etfs in RUB 1653 "futuresCostRUB": 0., # costs of all futures in RUB 1654 "Currencies": [], # list of dictionaries of all currencies statistics 1655 "Shares": [], # list of dictionaries of all shares statistics 1656 "Bonds": [], # list of dictionaries of all bonds statistics 1657 "Etfs": [], # list of dictionaries of all etfs statistics 1658 "Futures": [], # list of dictionaries of all futures statistics 1659 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1660 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1661 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1662 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1663 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1664 }, 1665 "analytics": { # --- some analytics of portfolio: 1666 "distrByAssets": {}, # portfolio distribution by assets 1667 "distrByCompanies": {}, # portfolio distribution by companies 1668 "distrBySectors": {}, # portfolio distribution by sectors 1669 "distrByCurrencies": {}, # portfolio distribution by currencies 1670 "distrByCountries": {}, # portfolio distribution by countries 1671 } 1672 } 1673 1674 details = details.lower() 1675 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1676 if details not in availableDetails: 1677 details = "full" 1678 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1679 1680 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1681 1682 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1683 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1684 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1685 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1686 1687 # save response headers without "positions" section: 1688 for key in portfolioResponse.keys(): 1689 if key != "positions": 1690 view["raw"]["headers"][key] = portfolioResponse[key] 1691 1692 else: 1693 continue 1694 1695 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1696 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1697 for item in portfolioResponse["positions"]: 1698 if item["instrumentType"] == "currency": 1699 self.figi = item["figi"] 1700 curr = self.SearchByFIGI(requestPrice=False) 1701 1702 # current price of currency in RUB: 1703 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1704 "name": curr["name"], 1705 "currentPrice": NanoToFloat( 1706 item["currentPrice"]["units"], 1707 item["currentPrice"]["nano"] 1708 ), 1709 } 1710 1711 view["raw"]["Currencies"].append(item) 1712 1713 elif item["instrumentType"] == "share": 1714 view["raw"]["Shares"].append(item) 1715 1716 elif item["instrumentType"] == "bond": 1717 view["raw"]["Bonds"].append(item) 1718 1719 elif item["instrumentType"] == "etf": 1720 view["raw"]["Etfs"].append(item) 1721 1722 elif item["instrumentType"] == "futures": 1723 view["raw"]["Futures"].append(item) 1724 1725 else: 1726 continue 1727 1728 # how many volume of currencies (by ISO currency name) are blocked: 1729 for item in view["raw"]["positions"]["blocked"]: 1730 blocked = NanoToFloat(item["units"], item["nano"]) 1731 if blocked > 0: 1732 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1733 1734 # how many volume of instruments (by FIGI) are blocked: 1735 for item in view["raw"]["positions"]["securities"]: 1736 blocked = int(item["blocked"]) 1737 if blocked > 0: 1738 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1739 1740 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1741 1742 if "rub" in allBlocked.keys(): 1743 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1744 1745 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1746 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1747 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1748 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1749 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1750 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1751 view["stat"]["portfolioCostRUB"] = sum([ 1752 view["stat"]["allCurrenciesCostRUB"], 1753 view["stat"]["sharesCostRUB"], 1754 view["stat"]["bondsCostRUB"], 1755 view["stat"]["etfsCostRUB"], 1756 view["stat"]["futuresCostRUB"], 1757 ]) 1758 1759 # --- calculating some portfolio statistics: 1760 byComp = {} # distribution by companies 1761 bySect = {} # distribution by sectors 1762 byCurr = {} # distribution by currencies (include RUB) 1763 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1764 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1765 1766 for item in portfolioResponse["positions"]: 1767 self.figi = item["figi"] 1768 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1769 1770 if instrument: 1771 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1772 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1773 1774 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1775 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1776 1777 else: 1778 blocked = 0 1779 1780 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1781 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1782 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1783 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1784 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1785 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1786 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1787 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1788 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1789 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1790 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1791 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1792 1793 statData = { 1794 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1795 "ticker": instrument["ticker"], # ticker by FIGI 1796 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1797 "volume": volume, # available volume of instrument 1798 "lots": lots, # volume in lots of instrument 1799 "direction": direction, # direction of an instrument's position: short or long 1800 "blocked": blocked, # blocked volume of currency or instrument 1801 "currentPrice": curPrice, # current instrument's price in basic asset 1802 "average": average, # current average position price 1803 "cost": cost, # current cost of all volume of instrument in basic asset 1804 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1805 "costRUB": costRUB, # cost of instrument in ruble 1806 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1807 "profit": profit, # expected profit at current moment 1808 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1809 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1810 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1811 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1812 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1813 "step": instrument["step"], # minimum price increment 1814 } 1815 1816 # adding distribution by unique countries: 1817 if statData["country"] not in byCountry.keys(): 1818 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1819 1820 else: 1821 byCountry[statData["country"]]["cost"] += costRUB 1822 byCountry[statData["country"]]["percent"] += percentCostRUB 1823 1824 if item["instrumentType"] != "currency": 1825 # adding distribution by unique companies: 1826 if statData["name"]: 1827 if statData["name"] not in byComp.keys(): 1828 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 byComp[statData["name"]]["cost"] += costRUB 1832 byComp[statData["name"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique sectors: 1835 if statData["sector"] not in bySect.keys(): 1836 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 bySect[statData["sector"]]["cost"] += costRUB 1840 bySect[statData["sector"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique currencies: 1843 if currency not in byCurr.keys(): 1844 byCurr[currency] = { 1845 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1846 "cost": costRUB, 1847 "percent": percentCostRUB 1848 } 1849 1850 else: 1851 byCurr[currency]["cost"] += costRUB 1852 byCurr[currency]["percent"] += percentCostRUB 1853 1854 # saving statistics for every instrument: 1855 if item["instrumentType"] == "currency": 1856 view["stat"]["Currencies"].append(statData) 1857 1858 # update dict with free funds for trading (total - blocked) by currencies 1859 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1860 view["stat"]["funds"][currency] = { 1861 "total": volume, 1862 "totalCostRUB": costRUB, # total volume cost in rubles 1863 "free": volume - blocked, 1864 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1865 } 1866 1867 elif item["instrumentType"] == "share": 1868 view["stat"]["Shares"].append(statData) 1869 1870 elif item["instrumentType"] == "bond": 1871 view["stat"]["Bonds"].append(statData) 1872 1873 elif item["instrumentType"] == "etf": 1874 view["stat"]["Etfs"].append(statData) 1875 1876 elif item["instrumentType"] == "Futures": 1877 view["stat"]["Futures"].append(statData) 1878 1879 else: 1880 continue 1881 1882 # total changes in Russian Ruble: 1883 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1884 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1885 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1886 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1887 view["stat"]["funds"]["rub"] = { 1888 "total": view["stat"]["availableRUB"], 1889 "totalCostRUB": view["stat"]["availableRUB"], 1890 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1892 } 1893 1894 # --- pending orders sector data: 1895 uniquePendingOrders = [] 1896 uniquePendingOrdersFIGIs = [] 1897 for item in view["raw"]["orders"]: 1898 if item["figi"] not in uniquePendingOrdersFIGIs: 1899 uniquePendingOrdersFIGIs.append(item["figi"]) 1900 uniquePendingOrders.append(item) 1901 1902 for item in uniquePendingOrders: 1903 self.figi = item["figi"] 1904 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrders = [] 1944 uniqueStopOrdersFIGIs = [] 1945 for item in view["raw"]["stopOrders"]: 1946 if item["figi"] not in uniqueStopOrdersFIGIs: 1947 uniqueStopOrdersFIGIs.append(item["figi"]) 1948 uniqueStopOrders.append(item) 1949 1950 for item in uniqueStopOrders: 1951 self.figi = item["figi"] 1952 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2055 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2056 2057 view["analytics"]["distrByCurrencies"].update(byCurr) 2058 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2059 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2060 2061 # portfolio distribution by countries: 2062 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2063 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2065 2066 view["analytics"]["distrByCountries"].update(byCountry) 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2068 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2069 2070 # --- Prepare text statistics overview in human-readable: 2071 if show: 2072 # Whatever the value `details`, header not changes: 2073 info = [ 2074 "# Client's portfolio\n\n", 2075 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2076 "* **Account ID:** [{}]\n".format(self.accountId), 2077 ] 2078 2079 if details in ["full", "positions", "digest"]: 2080 info.extend([ 2081 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2082 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2083 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2084 view["stat"]["totalChangesRUB"], 2085 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2086 view["stat"]["totalChangesPercentRUB"], 2087 ), 2088 ]) 2089 2090 if details in ["full", "positions"]: 2091 info.extend([ 2092 "## Open positions\n\n", 2093 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2094 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2095 "| Ruble | {:>31} | | | | | |\n".format( 2096 "{:.2f} ({:.2f}) rub".format( 2097 view["stat"]["availableRUB"], 2098 view["stat"]["blockedRUB"], 2099 ) 2100 ) 2101 ]) 2102 2103 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2104 return [ 2105 "| | | | | | | |\n", 2106 "| {:<27} | | | | | {:>19} | |\n".format( 2107 noTradeStr if noTradeStr else typeStr, 2108 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2109 ), 2110 ] 2111 2112 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2113 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2114 "{} [{}]".format(data["ticker"], data["figi"]), 2115 "{:.2f} ({:.2f}) {}".format( 2116 data["volume"], 2117 data["blocked"], 2118 data["currency"], 2119 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2120 data["volume"], 2121 data["blocked"], 2122 ), 2123 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2124 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2126 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2127 "{}{:.2f} {} ({}{:.2f}%)".format( 2128 "+" if data["profit"] > 0 else "", 2129 data["profit"], data["baseCurrencyName"], 2130 "+" if data["percentProfit"] > 0 else "", 2131 data["percentProfit"], 2132 ), 2133 ) 2134 2135 # --- Show currencies section: 2136 if view["stat"]["Currencies"]: 2137 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2138 for item in view["stat"]["Currencies"]: 2139 info.append(_InfoStr(item, showCurrencyName=True)) 2140 2141 else: 2142 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2143 2144 # --- Show shares section: 2145 if view["stat"]["Shares"]: 2146 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2147 2148 for item in view["stat"]["Shares"]: 2149 info.append(_InfoStr(item)) 2150 2151 else: 2152 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2153 2154 # --- Show bonds section: 2155 if view["stat"]["Bonds"]: 2156 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2157 2158 for item in view["stat"]["Bonds"]: 2159 info.append(_InfoStr(item)) 2160 2161 else: 2162 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2163 2164 # --- Show etfs section: 2165 if view["stat"]["Etfs"]: 2166 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2167 2168 for item in view["stat"]["Etfs"]: 2169 info.append(_InfoStr(item)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2173 2174 # --- Show futures section: 2175 if view["stat"]["Futures"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2177 2178 for item in view["stat"]["Futures"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2183 2184 if details in ["full", "orders"]: 2185 # --- Show pending orders section: 2186 if view["stat"]["orders"]: 2187 info.extend([ 2188 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2189 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2190 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2191 ]) 2192 2193 for item in view["stat"]["orders"]: 2194 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2195 "{} [{}]".format(item["ticker"], item["figi"]), 2196 item["orderID"], 2197 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2198 "{} {} ({}{:.2f}%)".format( 2199 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2200 item["baseCurrencyName"], 2201 "+" if item["percentChanges"] > 0 else "", 2202 float(item["percentChanges"]), 2203 ), 2204 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2205 item["action"], 2206 item["type"], 2207 item["date"], 2208 )) 2209 2210 else: 2211 info.append("\n## Total pending limit-orders: 0\n") 2212 2213 # --- Show stop orders section: 2214 if view["stat"]["stopOrders"]: 2215 info.extend([ 2216 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2217 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2218 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["stopOrders"]: 2222 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 item["lotsRequested"], 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2234 item["action"], 2235 item["type"], 2236 item["expType"], 2237 item["createDate"], 2238 item["expDate"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total stop-orders: 0\n") 2243 2244 if details in ["full", "analytics"]: 2245 # -- Show analytics section: 2246 if view["stat"]["portfolioCostRUB"] > 0: 2247 info.extend([ 2248 "\n# Analytics\n" 2249 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2250 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2251 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2252 view["stat"]["totalChangesRUB"], 2253 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2254 view["stat"]["totalChangesPercentRUB"], 2255 ), 2256 "\n## Portfolio distribution by assets\n" 2257 "\n| Type | Uniques | Percent | Current cost |\n", 2258 "|------------|---------|---------|--------------------|\n", 2259 ]) 2260 2261 for key in view["analytics"]["distrByAssets"].keys(): 2262 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2263 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2264 key, 2265 view["analytics"]["distrByAssets"][key]["uniques"], 2266 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2267 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2268 )) 2269 2270 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2271 info.extend([ 2272 "\n## Portfolio distribution by companies\n" 2273 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2274 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2275 ]) 2276 2277 for company in view["analytics"]["distrByCompanies"].keys(): 2278 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2279 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2280 info.append("| {} | {:<7} | {:<18} |\n".format( 2281 "{}{}{}".format( 2282 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2283 company, 2284 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2285 ), 2286 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2288 )) 2289 2290 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2291 info.extend([ 2292 "\n## Portfolio distribution by sectors\n" 2293 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2294 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2295 ]) 2296 2297 for sector in view["analytics"]["distrBySectors"].keys(): 2298 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2299 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2300 sector, 2301 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2302 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2304 )) 2305 2306 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2307 info.extend([ 2308 "\n## Portfolio distribution by currencies\n" 2309 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2310 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2311 ]) 2312 2313 for curr in view["analytics"]["distrByCurrencies"].keys(): 2314 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2315 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2316 info.append("| {} | {:<7} | {:<18} |\n".format( 2317 "[{}] {}{}".format( 2318 curr, 2319 view["analytics"]["distrByCurrencies"][curr]["name"], 2320 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2324 )) 2325 2326 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2327 info.extend([ 2328 "\n## Portfolio distribution by countries\n" 2329 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2330 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2331 ]) 2332 2333 for country in view["analytics"]["distrByCountries"].keys(): 2334 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2335 nameLen = len(country) 2336 info.append("| {} | {:<7} | {:<18} |\n".format( 2337 "{}{}".format( 2338 country, 2339 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2340 ), 2341 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2343 )) 2344 2345 infoText = "".join(info) 2346 2347 uLogger.info(infoText) 2348 2349 if details == "full" and self.overviewFile: 2350 filename = self.overviewFile 2351 2352 elif details == "digest" and self.overviewDigestFile: 2353 filename = self.overviewDigestFile 2354 2355 elif details == "positions" and self.overviewPositionsFile: 2356 filename = self.overviewPositionsFile 2357 2358 elif details == "orders" and self.overviewOrdersFile: 2359 filename = self.overviewOrdersFile 2360 2361 elif details == "analytics" and self.overviewAnalyticsFile: 2362 filename = self.overviewAnalyticsFile 2363 2364 else: 2365 filename = "" 2366 2367 if filename: 2368 with open(filename, "w", encoding="UTF-8") as fH: 2369 fH.write(infoText) 2370 2371 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2372 2373 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2375 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2376 """ 2377 Returns history operations between two given dates for current `accountId`. 2378 If `reportFile` string is not empty then also save human-readable report. 2379 Shows some statistical data of closed positions. 2380 2381 :param start: see docstring in `GetDatesAsString()` method 2382 :param end: see docstring in `GetDatesAsString()` method 2383 :param show: if `True` then also prints all records to the console. 2384 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2385 :return: original list of dictionaries with history of deals records from API ("operations" key): 2386 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2387 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2388 """ 2389 if self.accountId is None or not self.accountId: 2390 uLogger.error("Variable `accountId` must be defined for using this method!") 2391 raise Exception("Account ID required") 2392 2393 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2394 2395 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2396 2397 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2398 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2399 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2400 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2401 customStat = {} # custom statistics in additional to responseJSON 2402 2403 # --- output report in human-readable format: 2404 if show or self.reportFile: 2405 splitLine1 = "| | | | | |\n" # Summary section 2406 splitLine2 = "| | | | | | | | |\n" # Operations section 2407 nextDay = "" 2408 2409 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2410 2411 if len(ops) > 0: 2412 customStat = { 2413 "opsCount": 0, # total operations count 2414 "buyCount": 0, # buy operations 2415 "sellCount": 0, # sell operations 2416 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2417 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2418 "payIn": {"rub": 0.}, # Deposit brokerage account 2419 "payOut": {"rub": 0.}, # Withdrawals 2420 "divs": {"rub": 0.}, # Dividends income 2421 "coupons": {"rub": 0.}, # Coupon's income 2422 "brokerCom": {"rub": 0.}, # Service commissions 2423 "serviceCom": {"rub": 0.}, # Service commissions 2424 "marginCom": {"rub": 0.}, # Margin commissions 2425 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2426 } 2427 2428 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2429 for item in ops: 2430 if item["state"] == "OPERATION_STATE_EXECUTED": 2431 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2432 2433 # count buy operations: 2434 if "_BUY" in item["operationType"]: 2435 customStat["buyCount"] += 1 2436 2437 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2438 customStat["buyTotal"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["buyTotal"][item["payment"]["currency"]] = payment 2442 2443 # count sell operations: 2444 elif "_SELL" in item["operationType"]: 2445 customStat["sellCount"] += 1 2446 2447 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2448 customStat["sellTotal"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["sellTotal"][item["payment"]["currency"]] = payment 2452 2453 # count incoming operations: 2454 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2455 if item["payment"]["currency"] in customStat["payIn"].keys(): 2456 customStat["payIn"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["payIn"][item["payment"]["currency"]] = payment 2460 2461 # count withdrawals operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2463 if item["payment"]["currency"] in customStat["payOut"].keys(): 2464 customStat["payOut"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payOut"][item["payment"]["currency"]] = payment 2468 2469 # count dividends income: 2470 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2471 if item["payment"]["currency"] in customStat["divs"].keys(): 2472 customStat["divs"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["divs"][item["payment"]["currency"]] = payment 2476 2477 # count coupon's income: 2478 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2479 if item["payment"]["currency"] in customStat["coupons"].keys(): 2480 customStat["coupons"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["coupons"][item["payment"]["currency"]] = payment 2484 2485 # count broker commissions: 2486 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2487 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2488 customStat["brokerCom"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["brokerCom"][item["payment"]["currency"]] = payment 2492 2493 # count service commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2495 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2496 customStat["serviceCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["serviceCom"][item["payment"]["currency"]] = payment 2500 2501 # count margin commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2503 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2504 customStat["marginCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["marginCom"][item["payment"]["currency"]] = payment 2508 2509 # count withholding taxes: 2510 elif "_TAX" in item["operationType"]: 2511 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2512 customStat["allTaxes"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["allTaxes"][item["payment"]["currency"]] = payment 2516 2517 else: 2518 continue 2519 2520 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2521 2522 # --- view "Actions" lines: 2523 info.extend([ 2524 "| 1 | 2 | 3 | 4 | 5 |\n", 2525 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2526 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2527 "| | Buy: {:<22} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2530 ), 2531 "| | Sell: {:<21} | {:<28} | | |\n".format( 2532 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2533 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2534 ), 2535 ]) 2536 2537 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2538 for key in opsKeys: 2539 if key == "rub": 2540 continue 2541 2542 info.extend([ 2543 "| | | {:<28} | | |\n".format( 2544 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2545 ), 2546 "| | | {:<28} | | |\n".format( 2547 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2548 ), 2549 ]) 2550 2551 info.append(splitLine1) 2552 2553 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2554 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2555 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2558 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2559 ) 2560 2561 # --- view "Payments" lines: 2562 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2563 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2564 2565 for key in paymentsKeys: 2566 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2567 2568 info.append(splitLine1) 2569 2570 # --- view "Commissions and taxes" lines: 2571 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2572 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2573 2574 for key in comKeys: 2575 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2576 2577 info.append(splitLine1) 2578 2579 info.extend([ 2580 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2581 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2582 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2583 ]) 2584 2585 else: 2586 info.append("Broker returned no operations during this period\n") 2587 2588 # --- view "Operations" section: 2589 for item in ops: 2590 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2591 continue 2592 2593 else: 2594 self.figi = item["figi"] if item["figi"] else "" 2595 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2596 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2597 2598 # group of deals during one day: 2599 if nextDay and item["date"].split("T")[0] != nextDay: 2600 info.append(splitLine2) 2601 nextDay = "" 2602 2603 else: 2604 nextDay = item["date"].split("T")[0] # saving current day for splitting 2605 2606 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2607 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2608 self.figi if self.figi else "—", 2609 instrument["ticker"] if instrument else "—", 2610 instrument["type"] if instrument else "—", 2611 item["quantity"] if int(item["quantity"]) > 0 else "—", 2612 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2613 TKS_OPERATION_STATES[item["state"]], 2614 TKS_OPERATION_TYPES[item["operationType"]], 2615 )) 2616 2617 infoText = "".join(info) 2618 2619 if show: 2620 uLogger.info(infoText) 2621 2622 if self.reportFile: 2623 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2624 fH.write(infoText) 2625 2626 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2627 2628 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2630 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2631 """ 2632 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2633 2634 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2635 Warning! Broker server used ISO UTC time by default. 2636 2637 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2638 Also, `historyFile` used to update history with `onlyMissing` parameter. 2639 2640 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2641 2642 :param start: see docstring in `GetDatesAsString()` method. 2643 :param end: see docstring in `GetDatesAsString()` method. 2644 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2645 `"hour"`, `"day"`. Default: `"hour"`. 2646 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2647 False by default. Warning! History appends only from last candle to current time 2648 with always update last candle! 2649 :param csvSep: separator if csv-file is used, `,` by default. 2650 :param show: if `True` then also prints Pandas DataFrame to the console. 2651 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2652 `["date", "time", "open", "high", "low", "close", "volume"]`. 2653 """ 2654 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2655 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2656 history = None # empty pandas object for history 2657 2658 if interval not in TKS_CANDLE_INTERVALS.keys(): 2659 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2660 raise Exception("Incorrect value") 2661 2662 if not (self.ticker or self.figi): 2663 uLogger.error("Ticker or FIGI must be defined!") 2664 raise Exception("Ticker or FIGI required") 2665 2666 if self.ticker and not self.figi: 2667 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2668 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2669 2670 if self.figi and not self.ticker: 2671 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2672 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2673 2674 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2675 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2676 if interval.lower() != "day": 2677 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2678 2679 delta = dtEnd - dtStart # current UTC time minus last time in file 2680 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2681 2682 # calculate history length in candles: 2683 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2684 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2685 length += 1 # to avoid fraction time 2686 2687 # calculate data blocks count: 2688 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2689 2690 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2691 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2692 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2693 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2694 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2695 2696 tempOld = None # pandas object for old history, if --only-missing key present 2697 lastTime = None # datetime object of last old candle in file 2698 2699 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2700 uLogger.debug("--only-missing key present, add only last missing candles...") 2701 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2702 2703 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2704 2705 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2706 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2707 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2708 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2709 2710 # get last datetime object from last string in file or minus 1 delta if file is empty: 2711 if len(tempOld) > 0: 2712 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2713 2714 else: 2715 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2716 2717 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2718 2719 responseJSONs = [] # raw history blocks of data 2720 2721 blockEnd = dtEnd 2722 for item in range(blocks): 2723 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2724 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2725 2726 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2727 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2728 )) 2729 2730 if blockStart == blockEnd: 2731 uLogger.debug("Skipped this zero-length block...") 2732 2733 else: 2734 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2735 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2736 self.body = str({ 2737 "figi": self.figi, 2738 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2739 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2740 "interval": TKS_CANDLE_INTERVALS[interval][0] 2741 }) 2742 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2743 2744 if "code" in responseJSON.keys(): 2745 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2746 2747 else: 2748 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2749 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2750 2751 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2752 2753 blockEnd = blockStart 2754 2755 printCount = len(responseJSONs) # candles to show in console 2756 if responseJSONs: 2757 tempHistory = pd.DataFrame( 2758 data={ 2759 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2761 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2762 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2763 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2764 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2765 "volume": [int(item["volume"]) for item in responseJSONs], 2766 }, 2767 index=range(len(responseJSONs)), 2768 columns=["date", "time", "open", "high", "low", "close", "volume"], 2769 ) 2770 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2771 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2772 2773 # append only newest candles to old history if --only-missing key present: 2774 if onlyMissing and tempOld is not None and lastTime is not None: 2775 index = 0 # find start index in tempHistory data: 2776 2777 for i, item in tempHistory.iterrows(): 2778 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2779 2780 if curTime == lastTime: 2781 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2782 index = i 2783 printCount = index + 1 2784 break 2785 2786 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2787 2788 else: 2789 history = tempHistory # if no `--only-missing` key then load full data from server 2790 2791 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2792 2793 if history is not None and not history.empty: 2794 if show: 2795 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2796 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2797 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2798 )) 2799 2800 else: 2801 uLogger.warning("Received an empty candles history!") 2802 2803 if self.historyFile is not None: 2804 if history is not None and not history.empty: 2805 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2806 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2807 2808 else: 2809 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2810 2811 else: 2812 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2813 2814 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2816 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2817 """ 2818 Load candles history from csv-file and return Pandas DataFrame object. 2819 2820 See also: `History()` and `ShowHistoryChart()` methods. 2821 2822 :param filePath: path to csv-file to open. 2823 """ 2824 loadedHistory = None # init candles data object 2825 2826 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2827 2828 if os.path.exists(filePath): 2829 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2830 2831 tfStr = self.priceModel.FormattedDelta( 2832 self.priceModel.timeframe, 2833 "{days} days {hours}h {minutes}m {seconds}s", 2834 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2835 self.priceModel.timeframe, 2836 "{hours}h {minutes}m {seconds}s", 2837 ) 2838 2839 if loadedHistory is not None and not loadedHistory.empty: 2840 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2841 len(loadedHistory), 2842 tfStr, 2843 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2844 ) 2845 2846 else: 2847 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2848 2849 else: 2850 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2851 2852 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2854 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2855 """ 2856 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2857 2858 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2859 Default: `index.html` (both for interact and non-interact candlesticks chart). 2860 2861 See also: `History()` and `LoadHistory()` methods. 2862 2863 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2864 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2865 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2866 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2867 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2868 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2869 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2870 """ 2871 if isinstance(candles, str): 2872 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2873 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2874 2875 elif isinstance(candles, pd.DataFrame): 2876 self.priceModel.prices = candles # set candles chain from variable 2877 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2878 2879 if "datetime" not in candles.columns: 2880 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2881 2882 else: 2883 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2884 raise Exception("Incorrect value") 2885 2886 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2887 2888 if interact: 2889 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2890 2891 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2892 2893 else: 2894 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2895 2896 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2897 2898 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2900 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2901 """ 2902 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2903 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2904 2905 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2906 2907 :param operation: string "Buy" or "Sell". 2908 :param lots: volume, integer count of lots >= 1. 2909 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2910 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2911 :param expDate: string "Undefined" by default or local date in future, 2912 it is a string with format `%Y-%m-%d %H:%M:%S`. 2913 :return: JSON with response from broker server. 2914 """ 2915 if self.accountId is None or not self.accountId: 2916 uLogger.error("Variable `accountId` must be defined for using this method!") 2917 raise Exception("Account ID required") 2918 2919 if operation is None or not operation or operation not in ("Buy", "Sell"): 2920 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2921 raise Exception("Incorrect value") 2922 2923 if lots is None or lots < 1: 2924 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2925 lots = 1 2926 2927 if tp is None or tp < 0: 2928 tp = 0 2929 2930 if sl is None or sl < 0: 2931 sl = 0 2932 2933 if expDate is None or not expDate: 2934 expDate = "Undefined" 2935 2936 if not (self.ticker or self.figi): 2937 uLogger.error("Ticker or FIGI must be defined!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2941 self.ticker = instrument["ticker"] 2942 self.figi = instrument["figi"] 2943 2944 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2945 2946 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2947 self.body = str({ 2948 "figi": self.figi, 2949 "quantity": str(lots), 2950 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2951 "accountId": str(self.accountId), 2952 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2953 }) 2954 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2955 2956 if "orderId" in response.keys(): 2957 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2958 operation, response["orderId"], 2959 self.ticker, self.figi, lots, 2960 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2961 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2962 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2963 )) 2964 2965 else: 2966 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2967 2968 if tp > 0: 2969 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2970 2971 if sl > 0: 2972 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2973 2974 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2976 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2977 """ 2978 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2979 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2980 2981 See also: `Order()` and `Trade()` docstrings. 2982 2983 :param lots: volume, integer count of lots >= 1. 2984 :param tp: float > 0, take profit price of stop-order. 2985 :param sl: float > 0, stop loss price of stop-order. 2986 :param expDate: it's a local date in future. 2987 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2988 :return: JSON with response from broker server. 2989 """ 2990 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2992 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` and `Trade()` docstrings. 2998 2999 :param lots: volume, integer count of lots >= 1. 3000 :param tp: float > 0, take profit price of stop-order. 3001 :param sl: float > 0, stop loss price of stop-order. 3002 :param expDate: it's a local date in the future. 3003 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3004 :return: JSON with response from broker server. 3005 """ 3006 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3008 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3009 """ 3010 Close position of given instruments. 3011 3012 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3013 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3014 This avoids unnecessary downloading data from the server. 3015 """ 3016 if instruments is None or not instruments: 3017 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3018 raise Exception("Ticker or FIGI required") 3019 3020 if isinstance(instruments, str): 3021 instruments = [instruments] 3022 3023 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3024 if uniqueInstruments: 3025 if portfolio is None or not portfolio: 3026 portfolio = self.Overview(show=False) 3027 3028 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3029 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3030 3031 for self.figi in uniqueInstruments: 3032 if self.figi not in allOpened: 3033 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3034 continue 3035 3036 # search open trade info about instrument by ticker: 3037 instrument = {} 3038 for iType in TKS_INSTRUMENTS: 3039 if instrument: 3040 break 3041 3042 for item in portfolio["stat"][iType]: 3043 if item["figi"] == self.figi: 3044 instrument = item 3045 break 3046 3047 if instrument: 3048 self.ticker = instrument["ticker"] 3049 self.figi = instrument["figi"] 3050 3051 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3052 self.ticker, 3053 self.figi, 3054 int(instrument["volume"]), 3055 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3056 )) 3057 3058 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3059 3060 if tradeLots > 0: 3061 if instrument["blocked"] > 0: 3062 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3063 instrument["blocked"], 3064 self.ticker, 3065 tradeLots, 3066 )) 3067 3068 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3069 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3070 3071 else: 3072 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3074 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3075 """ 3076 Close all positions of given instruments with defined type. 3077 3078 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3079 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3080 This avoids unnecessary downloading data from the server. 3081 """ 3082 if iType not in TKS_INSTRUMENTS: 3083 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3084 3085 else: 3086 if portfolio is None or not portfolio: 3087 portfolio = self.Overview(show=False) 3088 3089 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3090 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3091 3092 if tickers and portfolio: 3093 self.CloseTrades(tickers, portfolio) 3094 3095 else: 3096 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3098 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3099 """ 3100 Universal method to create market or limit orders with all available parameters for current `accountId`. 3101 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3102 3103 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3104 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3105 3106 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3107 then broker immediately open market order as you can do simple --buy or --sell operations! 3108 3109 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3110 When current price will go up or down to target price value then broker opens a limit order. 3111 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3112 3113 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3114 3115 :param operation: string "Buy" or "Sell". 3116 :param orderType: string "Limit" or "Stop". 3117 :param lots: volume, integer count of lots >= 1. 3118 :param targetPrice: target price > 0. This is open trade price for limit order. 3119 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3120 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3121 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3122 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3123 Stop loss order always executed by market price. 3124 :param expDate: string "Undefined" by default or local date in future. 3125 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3126 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3127 A limit order has no expiration date, it lasts until the end of the trading day. 3128 :return: JSON with response from broker server. 3129 """ 3130 if self.accountId is None or not self.accountId: 3131 uLogger.error("Variable `accountId` must be defined for using this method!") 3132 raise Exception("Account ID required") 3133 3134 if operation is None or not operation or operation not in ("Buy", "Sell"): 3135 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3136 raise Exception("Incorrect value") 3137 3138 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3139 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3140 raise Exception("Incorrect value") 3141 3142 if lots is None or lots < 1: 3143 uLogger.error("You must define trade volume > 0: integer count of lots!") 3144 raise Exception("Incorrect value") 3145 3146 if targetPrice is None or targetPrice <= 0: 3147 uLogger.error("Target price for limit-order must be greater than 0!") 3148 raise Exception("Incorrect value") 3149 3150 if limitPrice is None or limitPrice <= 0: 3151 limitPrice = targetPrice 3152 3153 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3154 stopType = "Limit" 3155 3156 if expDate is None or not expDate: 3157 expDate = "Undefined" 3158 3159 if not (self.ticker or self.figi): 3160 uLogger.error("Tocker or FIGI must be defined!") 3161 raise Exception("Ticker or FIGI required") 3162 3163 response = {} 3164 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3165 self.ticker = instrument["ticker"] 3166 self.figi = instrument["figi"] 3167 3168 if orderType == "Limit": 3169 uLogger.debug( 3170 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3171 self.ticker, self.figi, 3172 operation, lots, targetPrice, instrument["currency"], 3173 )) 3174 3175 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3176 self.body = str({ 3177 "figi": self.figi, 3178 "quantity": str(lots), 3179 "price": FloatToNano(targetPrice), 3180 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3181 "accountId": str(self.accountId), 3182 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3183 }) 3184 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3185 3186 if "orderId" in response.keys(): 3187 uLogger.info( 3188 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3189 response["orderId"], 3190 self.ticker, self.figi, 3191 operation, lots, targetPrice, instrument["currency"], 3192 )) 3193 3194 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3195 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3196 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3197 targetPrice, instrument["currency"], 3198 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3199 )) 3200 3201 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3202 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3203 targetPrice, instrument["currency"], 3204 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3205 )) 3206 3207 else: 3208 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3209 3210 if orderType == "Stop": 3211 uLogger.debug( 3212 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3213 self.ticker, self.figi, 3214 operation, lots, 3215 targetPrice, instrument["currency"], 3216 limitPrice, instrument["currency"], 3217 stopType, expDate, 3218 )) 3219 3220 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3221 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3222 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3223 3224 body = { 3225 "figi": self.figi, 3226 "quantity": str(lots), 3227 "price": FloatToNano(limitPrice), 3228 "stopPrice": FloatToNano(targetPrice), 3229 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3230 "accountId": str(self.accountId), 3231 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3232 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3233 } 3234 3235 if expDateUTC: 3236 body["expireDate"] = expDateUTC 3237 3238 self.body = str(body) 3239 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3240 3241 if "stopOrderId" in response.keys(): 3242 uLogger.info( 3243 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3244 response["stopOrderId"], 3245 self.ticker, self.figi, 3246 operation, lots, 3247 targetPrice, instrument["currency"], 3248 limitPrice, instrument["currency"], 3249 TKS_STOP_ORDER_TYPES[stopOrderType], 3250 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3251 )) 3252 3253 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3254 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3255 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3256 targetPrice, instrument["currency"], 3257 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3258 )) 3259 3260 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3261 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3262 targetPrice, instrument["currency"], 3263 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3264 )) 3265 3266 else: 3267 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3268 3269 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3271 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3272 """ 3273 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3274 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3275 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3276 See also: `Order()` docstring. 3277 3278 :param lots: volume, integer count of lots >= 1. 3279 :param targetPrice: target price > 0. This is open trade price for limit order. 3280 :return: JSON with response from broker server. 3281 """ 3282 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3284 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3285 """ 3286 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3287 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3288 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3289 target price value then broker opens a limit order. See also: `Order()` docstring. 3290 3291 :param lots: volume, integer count of lots >= 1. 3292 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3293 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3294 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3295 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3296 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3297 :param expDate: string "Undefined" by default or local date in future. 3298 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3299 This date is converting to UTC format for server. 3300 :return: JSON with response from broker server. 3301 """ 3302 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3304 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3305 """ 3306 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3307 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3308 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3309 See also: `Order()` docstring. 3310 3311 :param lots: volume, integer count of lots >= 1. 3312 :param targetPrice: target price > 0. This is open trade price for limit order. 3313 :return: JSON with response from broker server. 3314 """ 3315 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3317 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3318 """ 3319 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3320 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3321 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3322 target price value then broker opens a limit order. See also: `Order()` docstring. 3323 3324 :param lots: volume, integer count of lots >= 1. 3325 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3326 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3327 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3328 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3329 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3330 :param expDate: string "Undefined" by default or local date in future. 3331 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3332 This date is converting to UTC format for server. 3333 :return: JSON with response from broker server. 3334 """ 3335 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3337 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3338 """ 3339 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3340 3341 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3342 :param allOrdersIDs: pre-received lists of all active pending orders. 3343 This avoids unnecessary downloading data from the server. 3344 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3345 """ 3346 if self.accountId is None or not self.accountId: 3347 uLogger.error("Variable `accountId` must be defined for using this method!") 3348 raise Exception("Account ID required") 3349 3350 if orderIDs: 3351 if allOrdersIDs is None or not allOrdersIDs: 3352 rawOrders = self.RequestPendingOrders() 3353 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3354 3355 if allStopOrdersIDs is None or not allStopOrdersIDs: 3356 rawStopOrders = self.RequestStopOrders() 3357 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3358 3359 for orderID in orderIDs: 3360 idInPendingOrders = orderID in allOrdersIDs 3361 idInStopOrders = orderID in allStopOrdersIDs 3362 3363 if not (idInPendingOrders or idInStopOrders): 3364 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3365 continue 3366 3367 else: 3368 if idInPendingOrders: 3369 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3370 3371 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3372 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3373 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3374 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3375 3376 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3377 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3378 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3379 3380 else: 3381 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3382 3383 elif idInStopOrders: 3384 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3385 3386 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3387 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3388 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3389 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3390 3391 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3392 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3393 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3394 3395 else: 3396 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3397 3398 else: 3399 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3401 def CloseAllOrders(self) -> None: 3402 """ 3403 Gets a list of open pending and stop orders and cancel it all. 3404 """ 3405 rawOrders = self.RequestPendingOrders() 3406 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3407 lenOrders = len(allOrdersIDs) 3408 3409 rawStopOrders = self.RequestStopOrders() 3410 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3411 lenSOrders = len(allStopOrdersIDs) 3412 3413 if lenOrders > 0 or lenSOrders > 0: 3414 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3415 3416 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3417 3418 else: 3419 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3421 def CloseAll(self, *args) -> None: 3422 """ 3423 Close all available (not blocked) opened trades and orders. 3424 3425 Also, you can select one or more keywords case-insensitive: 3426 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3427 3428 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3429 """ 3430 overview = self.Overview(show=False) # get all open trades info 3431 3432 if len(args) == 0: 3433 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3434 self.CloseAllOrders() # close all pending and stop orders 3435 3436 for iType in TKS_INSTRUMENTS: 3437 if iType != "Currencies": 3438 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3439 3440 else: 3441 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3442 lowerArgs = [x.lower() for x in args] 3443 3444 if "orders" in lowerArgs: 3445 self.CloseAllOrders() # close all pending and stop orders 3446 3447 for iType in TKS_INSTRUMENTS: 3448 if iType.lower() in lowerArgs and iType != "Currencies": 3449 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3451 @staticmethod 3452 def ParseOrderParameters(operation, **inputParameters): 3453 """ 3454 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3455 3456 :param operation: string "Buy" or "Sell". 3457 :param inputParameters: this is dict of strings that looks like this 3458 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3459 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3460 "prices" key: one or more prices to open limit-orders 3461 Counts of values in lots and prices lists must be equals! 3462 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3463 """ 3464 # TODO: update order grid work with api v2 3465 pass 3466 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3467 # 3468 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3469 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3470 # raise Exception("Incorrect value") 3471 # 3472 # if "l" in inputParameters.keys(): 3473 # inputParameters["lots"] = inputParameters.pop("l") 3474 # 3475 # if "p" in inputParameters.keys(): 3476 # inputParameters["prices"] = inputParameters.pop("p") 3477 # 3478 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3479 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3480 # raise Exception("Incorrect value") 3481 # 3482 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3483 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3484 # 3485 # if len(lots) != len(prices): 3486 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3487 # raise Exception("Incorrect value") 3488 # 3489 # uLogger.debug("Extracted parameters for orders:") 3490 # uLogger.debug("lots = {}".format(lots)) 3491 # uLogger.debug("prices = {}".format(prices)) 3492 # 3493 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3494 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3495 # uLogger.debug("Order parameters: {}".format(result)) 3496 # 3497 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3499 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3500 """ 3501 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3502 3503 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3504 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3505 """ 3506 result = False 3507 msg = "Instrument not defined!" 3508 3509 if portfolio is None or not portfolio: 3510 portfolio = self.Overview(show=False) 3511 3512 if self.ticker: 3513 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3514 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3515 3516 for iType in TKS_INSTRUMENTS: 3517 for instrument in portfolio["stat"][iType]: 3518 if instrument["ticker"] == self.ticker: 3519 result = True 3520 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3521 break 3522 3523 elif self.figi: 3524 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3525 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3526 3527 for iType in TKS_INSTRUMENTS: 3528 for instrument in portfolio["stat"][iType]: 3529 if instrument["figi"] == self.figi: 3530 result = True 3531 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3532 break 3533 3534 else: 3535 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3536 3537 uLogger.debug(msg) 3538 3539 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3541 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3542 """ 3543 Returns instrument is in the user's portfolio if it presents there. 3544 Instrument must be defined by `ticker` (highly priority) or `figi`. 3545 3546 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3547 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3548 """ 3549 result = None 3550 msg = "Instrument not defined!" 3551 3552 if portfolio is None or not portfolio: 3553 portfolio = self.Overview(show=False) 3554 3555 if self.ticker: 3556 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3557 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3558 3559 for iType in TKS_INSTRUMENTS: 3560 for instrument in portfolio["stat"][iType]: 3561 if instrument["ticker"] == self.ticker: 3562 result = instrument 3563 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3564 break 3565 3566 elif self.figi: 3567 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3568 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3569 3570 for iType in TKS_INSTRUMENTS: 3571 for instrument in portfolio["stat"][iType]: 3572 if instrument["figi"] == self.figi: 3573 result = instrument 3574 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3575 break 3576 3577 else: 3578 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3579 3580 uLogger.debug(msg) 3581 3582 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3584 def RequestLimits(self) -> dict: 3585 """ 3586 Method for obtaining the available funds for withdrawal for current `accountId`. 3587 3588 See also: 3589 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3590 - `OverviewLimits()` method 3591 3592 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3593 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3594 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3595 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3596 """ 3597 if self.accountId is None or not self.accountId: 3598 uLogger.error("Variable `accountId` must be defined for using this method!") 3599 raise Exception("Account ID required") 3600 3601 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3602 3603 self.body = str({"accountId": self.accountId}) 3604 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3605 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3606 3607 uLogger.debug("Records about available funds for withdrawal successfully received") 3608 3609 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3611 def OverviewLimits(self, show: bool = False) -> dict: 3612 """ 3613 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3614 3615 See also: `RequestLimits()`. 3616 3617 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3618 :return: dict with raw parsed data from server and some calculated statistics about it. 3619 """ 3620 if self.accountId is None or not self.accountId: 3621 uLogger.error("Variable `accountId` must be defined for using this method!") 3622 raise Exception("Account ID required") 3623 3624 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3625 3626 view = { 3627 "rawLimits": rawLimits, 3628 "limits": { # parsed data for every currency: 3629 "money": { # this is an array of portfolio currency positions 3630 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3631 }, 3632 "blocked": { # this is an array of blocked currency 3633 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3634 }, 3635 "blockedGuarantee": { # this is locked money under collateral for futures 3636 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3637 }, 3638 }, 3639 } 3640 3641 # --- Prepare text table with limits in human-readable format: 3642 if show: 3643 info = [ 3644 "# Withdrawal limits\n\n", 3645 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3646 "* **Account ID:** [{}]\n".format(self.accountId), 3647 ] 3648 3649 if view["limits"]["money"]: 3650 info.extend([ 3651 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3652 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3653 ]) 3654 3655 else: 3656 info.append("\nNo withdrawal limits\n") 3657 3658 for curr in view["limits"]["money"].keys(): 3659 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3660 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3661 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3662 3663 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3664 "[{}]".format(curr), 3665 "{:.2f}".format(view["limits"]["money"][curr]), 3666 "{:.2f}".format(availableMoney), 3667 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3668 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3669 ) 3670 3671 if curr == "rub": 3672 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3673 3674 else: 3675 info.append(infoStr) 3676 3677 infoText = "".join(info) 3678 3679 uLogger.info(infoText) 3680 3681 if self.withdrawalLimitsFile: 3682 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3683 fH.write(infoText) 3684 3685 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3686 3687 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3689 def RequestAccounts(self) -> dict: 3690 """ 3691 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3692 3693 See also: 3694 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3695 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3696 - `OverviewUserInfo()` method 3697 3698 :return: dict with raw data from server that contains accounts info. Example of dict: 3699 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3700 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3701 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3702 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3703 """ 3704 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3705 3706 self.body = str({}) 3707 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3708 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3709 3710 uLogger.debug("Records about available accounts successfully received") 3711 3712 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3714 def RequestUserInfo(self) -> dict: 3715 """ 3716 Method for requesting common user's information. 3717 3718 See also: 3719 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3720 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3721 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3722 - `OverviewUserInfo()` method 3723 3724 :return: dict with raw data from server that contains user's information. Example of dict: 3725 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3726 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3727 """ 3728 uLogger.debug("Requesting common user's information. Wait, please...") 3729 3730 self.body = str({}) 3731 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3732 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3733 3734 uLogger.debug("Records about current user successfully received") 3735 3736 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3738 def RequestMarginStatus(self, accountId: str = None) -> dict: 3739 """ 3740 Method for requesting margin calculation for defined account ID. 3741 3742 See also: 3743 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3744 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3745 - `OverviewUserInfo()` method 3746 3747 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3748 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3749 Example of responses: 3750 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3751 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3752 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3753 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3754 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3755 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3756 """ 3757 if accountId is None or not accountId: 3758 if self.accountId is None or not self.accountId: 3759 uLogger.error("Variable `accountId` must be defined for using this method!") 3760 raise Exception("Account ID required") 3761 3762 else: 3763 accountId = self.accountId # use `self.accountId` (main ID) by default 3764 3765 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3766 3767 self.body = str({"accountId": accountId}) 3768 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3769 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3770 3771 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3772 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3773 rawMargin = {} 3774 3775 else: 3776 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3777 3778 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3780 def RequestTariffLimits(self) -> dict: 3781 """ 3782 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3783 3784 See also: 3785 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3786 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3787 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3788 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3789 - `OverviewUserInfo()` method 3790 3791 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3792 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3793 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3794 """ 3795 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3796 3797 self.body = str({}) 3798 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3799 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3800 3801 uLogger.debug("Records with limits of current tariff successfully received") 3802 3803 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3805 def RequestBondCoupons(self, iJSON: dict) -> dict: 3806 """ 3807 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3808 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3809 All dates are in UTC timezone. 3810 3811 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3812 Documentation: 3813 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3814 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3815 3816 See also: `ExtendBondsData()`. 3817 3818 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3819 If raw iJSON is not data of bond then server returns an error [400] with message: 3820 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3821 :return: dictionary with bond payment calendar. Response example 3822 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3823 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3824 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3825 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3826 """ 3827 if iJSON["figi"] is None or not iJSON["figi"]: 3828 uLogger.error("FIGI must be defined for using this method!") 3829 raise Exception("FIGI required") 3830 3831 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3832 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3833 3834 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3835 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3836 self.figi, 3837 startDate, 3838 endDate, 3839 )) 3840 3841 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3842 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3843 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3844 3845 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3846 uLogger.warning("Instrument type is not bond!") 3847 3848 else: 3849 uLogger.debug("Records about bond payment calendar successfully received") 3850 3851 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3853 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3854 """ 3855 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3856 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3857 coupon yields, current yields and some statistics etc. 3858 3859 WARNING! This is too long operation if a lot of bonds requested from broker server. 3860 3861 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3862 3863 :param instruments: list of strings with tickers or FIGIs. 3864 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3865 for further used by data scientists or stock analytics. 3866 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3867 In XLSX-file and Pandas DataFrame fields mean: 3868 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3869 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3870 """ 3871 if instruments is None or not instruments: 3872 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3873 raise Exception("Ticker or FIGI required") 3874 3875 if isinstance(instruments, str): 3876 instruments = [instruments] 3877 3878 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3879 3880 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3881 3882 iCount = len(uniqueInstruments) 3883 tooLong = iCount >= 20 3884 if tooLong: 3885 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3886 3887 bonds = None 3888 for i, self.figi in enumerate(uniqueInstruments): 3889 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3890 3891 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3892 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3893 rawBond = self.SearchByFIGI(requestPrice=True) 3894 3895 # Widen raw data with UTC current time (iData["actualDateTime"]): 3896 actualDate = datetime.now(tzutc()) 3897 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3898 3899 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3900 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3901 3902 # Replace some values with human-readable: 3903 iData["nominalCurrency"] = iData["nominal"]["currency"] 3904 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3905 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3906 iData["aciCurrency"] = iData["aciValue"]["currency"] 3907 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3908 iData["issueSize"] = int(iData["issueSize"]) 3909 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3910 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3911 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3912 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3913 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3914 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3915 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3916 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3917 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3918 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3919 3920 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3921 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3922 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3923 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3924 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3925 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3926 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3927 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3928 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3929 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3930 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3931 3932 # Widen raw data with calendar data from `rawCalendar` values: 3933 calendarData = [] 3934 for item in iData["rawCalendar"]["events"]: 3935 calendarData.append({ 3936 "couponDate": item["couponDate"], 3937 "couponNumber": int(item["couponNumber"]), 3938 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3939 "payCurrency": item["payOneBond"]["currency"], 3940 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3941 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3942 "couponStartDate": item["couponStartDate"], 3943 "couponEndDate": item["couponEndDate"], 3944 "couponPeriod": item["couponPeriod"], 3945 }) 3946 3947 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3948 if "maturityDate" not in iData.keys(): 3949 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3950 3951 # Widen raw data with Coupon Rate. 3952 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3953 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3954 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3955 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3956 3957 # Widen raw data with Yield to Maturity (YTM) on current date. 3958 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3959 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3960 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3961 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3962 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3963 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3964 3965 iData["calendar"] = calendarData # adds calendar at the end 3966 3967 # Remove not used data: 3968 iData.pop("uid") 3969 iData.pop("positionUid") 3970 iData.pop("currentPrice") 3971 iData.pop("rawCalendar") 3972 3973 colNames = list(iData.keys()) 3974 if bonds is None: 3975 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3976 3977 else: 3978 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3979 3980 else: 3981 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3982 3983 processed = round(100 * (i + 1) / iCount, 1) 3984 if tooLong and processed % 5 == 0: 3985 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3986 3987 else: 3988 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3989 3990 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3991 3992 # Saving bonds from Pandas DataFrame to XLSX sheet: 3993 if xlsx and self.bondsXLSXFile: 3994 with pd.ExcelWriter( 3995 path=self.bondsXLSXFile, 3996 date_format=TKS_DATE_FORMAT, 3997 datetime_format=TKS_DATE_TIME_FORMAT, 3998 mode="w", 3999 ) as writer: 4000 bonds.to_excel( 4001 writer, 4002 sheet_name="Extended bonds data", 4003 index=True, 4004 encoding="UTF-8", 4005 freeze_panes=(1, 1), 4006 ) # saving as XLSX-file with freeze first row and column as headers 4007 4008 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4009 4010 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4012 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4013 """ 4014 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4015 4016 WARNING! This is too long operation if a lot of bonds requested from broker server. 4017 4018 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4019 4020 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4021 extended information about bonds: main info, current prices, bond payment calendar, 4022 coupon yields, current yields and some statistics etc. 4023 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4024 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4025 for further used by data scientists or stock analytics. 4026 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4027 """ 4028 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4029 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4030 4031 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4032 4033 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4034 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4035 calendar = None 4036 for bond in extBonds.iterrows(): 4037 for item in bond[1]["calendar"]: 4038 cData = { 4039 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4040 "couponDate": item["couponDate"], 4041 "figi": bond[1]["figi"], 4042 "ticker": bond[1]["ticker"], 4043 "name": bond[1]["name"], 4044 "couponNumber": item["couponNumber"], 4045 "payOneBond": item["payOneBond"], 4046 "payCurrency": item["payCurrency"], 4047 "couponType": item["couponType"], 4048 "couponPeriod": item["couponPeriod"], 4049 "fixDate": item["fixDate"], 4050 "couponStartDate": item["couponStartDate"], 4051 "couponEndDate": item["couponEndDate"], 4052 } 4053 4054 if calendar is None: 4055 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4056 4057 else: 4058 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4059 4060 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4061 4062 # Saving calendar from Pandas DataFrame to XLSX sheet: 4063 if xlsx: 4064 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4065 4066 with pd.ExcelWriter( 4067 path=xlsxCalendarFile, 4068 date_format=TKS_DATE_FORMAT, 4069 datetime_format=TKS_DATE_TIME_FORMAT, 4070 mode="w", 4071 ) as writer: 4072 humanReadable = calendar.copy(deep=True) 4073 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4074 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4075 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4076 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4077 humanReadable.columns = colNames # human-readable column names 4078 4079 humanReadable.to_excel( 4080 writer, 4081 sheet_name="Bond payments calendar", 4082 index=False, 4083 encoding="UTF-8", 4084 freeze_panes=(1, 2), 4085 ) # saving as XLSX-file with freeze first row and column as headers 4086 4087 del humanReadable # release df in memory 4088 4089 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4090 4091 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4093 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4094 """ 4095 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4096 Also, creates Markdown file with calendar data, `calendar.md` by default. 4097 4098 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4099 4100 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4101 extended information about bonds: main info, current prices, bond payment calendar, 4102 coupon yields, current yields and some statistics etc. 4103 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4104 :param show: if `True` then also printing bonds payment calendar to the console, 4105 otherwise save to file `calendarFile` only. `False` by default. 4106 :return: multilines text in Markdown format with bonds payment calendar as a table. 4107 """ 4108 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4109 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4110 4111 infoText = "# Bond payments calendar\n\n" 4112 4113 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4114 4115 if not calendar.empty: 4116 splitLine = "| | | | | | | | | |\n" 4117 4118 info = [ 4119 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4120 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4121 ] 4122 4123 newMonth = False 4124 notOneBond = calendar["figi"].nunique() > 1 4125 for i, bond in enumerate(calendar.iterrows()): 4126 if newMonth and notOneBond: 4127 info.append(splitLine) 4128 4129 info.append( 4130 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4131 " √" if bond[1]["paid"] else " —", 4132 bond[1]["couponDate"].split("T")[0], 4133 bond[1]["figi"], 4134 bond[1]["ticker"], 4135 bond[1]["couponNumber"], 4136 "{} {}".format( 4137 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4138 bond[1]["payCurrency"], 4139 ), 4140 bond[1]["couponType"], 4141 bond[1]["couponPeriod"], 4142 bond[1]["fixDate"].split("T")[0], 4143 ) 4144 ) 4145 4146 if i < len(calendar.values) - 1: 4147 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4148 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4149 newMonth = False if curDate.month == nextDate.month else True 4150 4151 else: 4152 newMonth = False 4153 4154 infoText += "".join(info) 4155 4156 if show: 4157 uLogger.info("{}".format(infoText)) 4158 4159 if self.calendarFile is not None: 4160 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4161 fH.write(infoText) 4162 4163 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4164 4165 else: 4166 infoText += "No data\n" 4167 4168 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4170 def OverviewAccounts(self, show: bool = False) -> dict: 4171 """ 4172 Method for parsing and show simple table with all available user accounts. 4173 4174 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4175 4176 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4177 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4178 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4179 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4180 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4181 "closed": "—", "access": "Full access" }, ...}}` 4182 """ 4183 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4184 4185 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4186 accounts = { 4187 item["id"]: { 4188 "type": TKS_ACCOUNT_TYPES[item["type"]], 4189 "name": item["name"], 4190 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4191 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4192 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4193 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4194 } for item in rawAccounts["accounts"] 4195 } 4196 4197 # Raw and parsed data with some fields replaced in "stat" section: 4198 view = { 4199 "rawAccounts": rawAccounts, 4200 "stat": accounts, 4201 } 4202 4203 # --- Prepare simple text table with only accounts data in human-readable format: 4204 if show: 4205 info = [ 4206 "# User accounts\n\n", 4207 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4208 "| Account ID | Type | Status | Name |\n", 4209 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4210 ] 4211 4212 for account in view["stat"].keys(): 4213 info.extend([ 4214 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4215 account, 4216 view["stat"][account]["type"], 4217 view["stat"][account]["status"], 4218 view["stat"][account]["name"], 4219 ) 4220 ]) 4221 4222 infoText = "".join(info) 4223 4224 uLogger.info(infoText) 4225 4226 if self.userAccountsFile: 4227 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4228 fH.write(infoText) 4229 4230 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4231 4232 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4234 def OverviewUserInfo(self, show: bool = False) -> dict: 4235 """ 4236 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4237 4238 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4239 4240 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4241 :return: dict with raw parsed data from server and some calculated statistics about it. 4242 """ 4243 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4244 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4245 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4246 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4247 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4248 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4249 4250 # This is dict with parsed common user data: 4251 userInfo = { 4252 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4253 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4254 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4255 "tariff": rawUserInfo["tariff"], 4256 } 4257 4258 # This is an array of dict with parsed margin statuses for every account IDs: 4259 margins = {} 4260 for accountId in accounts.keys(): 4261 if rawMargins[accountId]: 4262 margins[accountId] = { 4263 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4264 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4265 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4266 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4267 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4268 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4269 } 4270 4271 else: 4272 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4273 4274 unary = {} # unary-connection limits 4275 for item in rawTariffLimits["unaryLimits"]: 4276 if item["limitPerMinute"] in unary.keys(): 4277 unary[item["limitPerMinute"]].extend(item["methods"]) 4278 4279 else: 4280 unary[item["limitPerMinute"]] = item["methods"] 4281 4282 stream = {} # stream-connection limits 4283 for item in rawTariffLimits["streamLimits"]: 4284 if item["limit"] in stream.keys(): 4285 stream[item["limit"]].extend(item["streams"]) 4286 4287 else: 4288 stream[item["limit"]] = item["streams"] 4289 4290 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4291 limits = { 4292 "unary": unary, 4293 "stream": stream, 4294 } 4295 4296 # Raw and parsed data as an output result: 4297 view = { 4298 "rawUserInfo": rawUserInfo, 4299 "rawAccounts": rawAccounts, 4300 "rawMargins": rawMargins, 4301 "rawTariffLimits": rawTariffLimits, 4302 "stat": { 4303 "userInfo": userInfo, 4304 "accounts": accounts, 4305 "margins": margins, 4306 "limits": limits, 4307 }, 4308 } 4309 4310 # --- Prepare text table with user information in human-readable format: 4311 if show: 4312 info = [ 4313 "# Full user information\n\n", 4314 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4315 "## Common information\n\n", 4316 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4317 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4318 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4319 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4320 "\n## User accounts\n\n", 4321 ] 4322 4323 for account in view["stat"]["accounts"].keys(): 4324 info.extend([ 4325 "### ID: [{}]\n\n".format(account), 4326 "| Parameters | Values |\n", 4327 "|----------------------|--------------------------------------------------------------|\n", 4328 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4329 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4330 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4331 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4332 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4333 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4334 ]) 4335 4336 if margins[account]: 4337 info.extend([ 4338 "| Margin status: | Enabled |\n", 4339 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4340 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4341 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4342 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4343 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4344 ]) 4345 4346 else: 4347 info.append("| Margin status: | Disabled |\n\n") 4348 4349 info.extend([ 4350 "\n## Current user tariff limits\n", 4351 "\nSee also:\n", 4352 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4353 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4354 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4355 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4356 "\n### Unary limits\n", 4357 ]) 4358 4359 if unary: 4360 for key, values in sorted(unary.items()): 4361 info.append("\n* Max requests per minute: {}\n".format(key)) 4362 4363 for value in values: 4364 info.append(" - {}\n".format(value)) 4365 4366 else: 4367 info.append("\nNot available\n") 4368 4369 info.append("\n### Stream limits\n") 4370 4371 if stream: 4372 for key, values in sorted(stream.items()): 4373 info.append("\n* Max stream connections: {}\n".format(key)) 4374 4375 for value in values: 4376 info.append(" - {}\n".format(value)) 4377 4378 else: 4379 info.append("\nNot available\n") 4380 4381 infoText = "".join(info) 4382 4383 uLogger.info(infoText) 4384 4385 if self.userInfoFile: 4386 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4387 fH.write(infoText) 4388 4389 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4390 4391 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4394class Args: 4395 """ 4396 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4397 """ 4398 def __init__(self, **kwargs): 4399 self.__dict__.update(kwargs) 4400 4401 def __getattr__(self, item): 4402 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4405def ParseArgs(): 4406 """This function get and parse command line keys.""" 4407 parser = ArgumentParser() # command-line string parser 4408 4409 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4410 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4411 4412 # --- options: 4413 4414 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4415 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4416 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4417 4418 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4419 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4420 4421 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4422 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4423 4424 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4425 4426 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4427 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4428 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4429 4430 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4431 4432 # --- commands: 4433 4434 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4435 4436 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4437 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4438 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4439 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4440 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4441 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4442 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4443 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4444 4445 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4446 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4447 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4448 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4449 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4450 4451 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4452 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4453 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4454 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4455 4456 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4457 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4458 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4459 4460 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4461 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4462 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4463 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4464 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4465 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4466 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4467 4468 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4469 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4470 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4471 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4472 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4473 4474 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4475 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4476 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4477 4478 cmdArgs = parser.parse_args() 4479 return cmdArgs
This function get and parse command line keys.
4482def Main(**kwargs): 4483 """ 4484 Main function for work with TKSBrokerAPI in the console. 4485 4486 See examples: 4487 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4488 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4489 """ 4490 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4491 4492 if args.debug_level: 4493 uLogger.level = 10 # always debug level by default 4494 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4495 4496 exitCode = 0 4497 start = datetime.now(tzutc()) 4498 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4499 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4500 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4501 )) 4502 4503 # trying to calculate full current version: 4504 buildVersion = __version__ 4505 try: 4506 v = version("tksbrokerapi") 4507 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4508 4509 except Exception: 4510 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4511 4512 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4513 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4514 4515 try: 4516 if args.version: 4517 print("TKSBrokerAPI {}".format(buildVersion)) 4518 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4519 4520 else: 4521 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4522 server = TinkoffBrokerServer( 4523 token=args.token, 4524 accountId=args.account_id, 4525 useCache=not args.no_cache, 4526 ) 4527 4528 # --- set some options: 4529 4530 if args.ticker: 4531 if args.ticker in server.aliasesKeys: 4532 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4533 4534 else: 4535 server.ticker = args.ticker 4536 4537 if args.figi: 4538 server.figi = args.figi 4539 4540 if args.depth is not None: 4541 server.depth = args.depth 4542 4543 # --- do one of commands: 4544 4545 if args.list: 4546 if args.output is not None: 4547 server.instrumentsFile = args.output 4548 4549 server.ShowInstrumentsInfo(show=True) 4550 4551 elif args.list_xlsx: 4552 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4553 4554 elif args.bonds_xlsx is not None: 4555 if args.output is not None: 4556 server.bondsXLSXFile = args.output 4557 4558 if len(args.bonds_xlsx) == 0: 4559 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4560 4561 else: 4562 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4563 4564 elif args.search: 4565 if args.output is not None: 4566 server.searchResultsFile = args.output 4567 4568 server.SearchInstruments(pattern=args.search[0], show=True) 4569 4570 elif args.info: 4571 if not (args.ticker or args.figi): 4572 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4573 raise Exception("Ticker or FIGI required") 4574 4575 if args.output is not None: 4576 server.infoFile = args.output 4577 4578 if args.ticker: 4579 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4580 4581 else: 4582 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4583 4584 elif args.calendar is not None: 4585 if args.output is not None: 4586 server.calendarFile = args.output 4587 4588 if len(args.calendar) == 0: 4589 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4590 4591 else: 4592 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4593 4594 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4595 4596 elif args.price: 4597 if not (args.ticker or args.figi): 4598 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4599 raise Exception("Ticker or FIGI required") 4600 4601 server.GetCurrentPrices(show=True) 4602 4603 elif args.prices is not None: 4604 if args.output is not None: 4605 server.pricesFile = args.output 4606 4607 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4608 4609 elif args.overview: 4610 if args.output is not None: 4611 server.overviewFile = args.output 4612 4613 server.Overview(show=True, details="full") 4614 4615 elif args.overview_digest: 4616 if args.output is not None: 4617 server.overviewDigestFile = args.output 4618 4619 server.Overview(show=True, details="digest") 4620 4621 elif args.overview_positions: 4622 if args.output is not None: 4623 server.overviewPositionsFile = args.output 4624 4625 server.Overview(show=True, details="positions") 4626 4627 elif args.overview_orders: 4628 if args.output is not None: 4629 server.overviewOrdersFile = args.output 4630 4631 server.Overview(show=True, details="orders") 4632 4633 elif args.overview_analytics: 4634 if args.output is not None: 4635 server.overviewAnalyticsFile = args.output 4636 4637 server.Overview(show=True, details="analytics") 4638 4639 elif args.deals is not None: 4640 if args.output is not None: 4641 server.reportFile = args.output 4642 4643 if 0 <= len(args.deals) < 3: 4644 server.Deals( 4645 start=args.deals[0] if len(args.deals) >= 1 else None, 4646 end=args.deals[1] if len(args.deals) == 2 else None, 4647 show=True, # Always show deals report in console 4648 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4649 ) 4650 4651 else: 4652 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4653 raise Exception("Incorrect value") 4654 4655 elif args.history is not None: 4656 if args.output is not None: 4657 server.historyFile = args.output 4658 4659 if 0 <= len(args.history) < 3: 4660 dataReceived = server.History( 4661 start=args.history[0] if len(args.history) >= 1 else None, 4662 end=args.history[1] if len(args.history) == 2 else None, 4663 interval="hour" if args.interval is None or not args.interval else args.interval, 4664 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4665 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4666 show=True, # shows all downloaded candles in console 4667 ) 4668 4669 if args.render_chart is not None and dataReceived is not None: 4670 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4671 4672 server.ShowHistoryChart( 4673 candles=dataReceived, 4674 interact=iChart, 4675 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4676 ) 4677 4678 else: 4679 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4680 raise Exception("Incorrect value") 4681 4682 elif args.load_history is not None: 4683 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4684 4685 if args.render_chart is not None and histData is not None: 4686 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4687 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4688 4689 server.ShowHistoryChart( 4690 candles=histData, 4691 interact=iChart, 4692 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4693 ) 4694 4695 elif args.trade is not None: 4696 if 1 <= len(args.trade) <= 5: 4697 server.Trade( 4698 operation=args.trade[0], 4699 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4700 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4701 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4702 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4703 ) 4704 4705 else: 4706 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4707 4708 elif args.buy is not None: 4709 if 0 <= len(args.buy) <= 4: 4710 server.Buy( 4711 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4712 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4713 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4714 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4715 ) 4716 4717 else: 4718 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4719 4720 elif args.sell is not None: 4721 if 0 <= len(args.sell) <= 4: 4722 server.Sell( 4723 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4724 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4725 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4726 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4727 ) 4728 4729 else: 4730 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4731 4732 elif args.order: 4733 if 4 <= len(args.order) <= 7: 4734 server.Order( 4735 operation=args.order[0], 4736 orderType=args.order[1], 4737 lots=int(args.order[2]), 4738 targetPrice=float(args.order[3]), 4739 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4740 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4741 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4742 ) 4743 4744 else: 4745 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4746 4747 elif args.buy_limit: 4748 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4749 4750 elif args.sell_limit: 4751 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4752 4753 elif args.buy_stop: 4754 if 2 <= len(args.buy_stop) <= 7: 4755 server.BuyStop( 4756 lots=int(args.buy_stop[0]), 4757 targetPrice=float(args.buy_stop[1]), 4758 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4759 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4760 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4761 ) 4762 4763 else: 4764 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4765 4766 elif args.sell_stop: 4767 if 2 <= len(args.sell_stop) <= 7: 4768 server.SellStop( 4769 lots=int(args.sell_stop[0]), 4770 targetPrice=float(args.sell_stop[1]), 4771 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4772 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4773 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4774 ) 4775 4776 else: 4777 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4778 4779 # elif args.buy_order_grid is not None: 4780 # # update order grid work with api v2 4781 # if len(args.buy_order_grid) == 2: 4782 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4783 # 4784 # for order in orderParams: 4785 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4786 # 4787 # else: 4788 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4789 # 4790 # elif args.sell_order_grid is not None: 4791 # # update order grid work with api v2 4792 # if len(args.sell_order_grid) >= 2: 4793 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4794 # 4795 # for order in orderParams: 4796 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4797 # 4798 # else: 4799 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4800 4801 elif args.close_order is not None: 4802 server.CloseOrders(args.close_order) # close only one order 4803 4804 elif args.close_orders is not None: 4805 server.CloseOrders(args.close_orders) # close list of orders 4806 4807 elif args.close_trade: 4808 if not (args.ticker or args.figi): 4809 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4810 raise Exception("Ticker or FIGI required") 4811 4812 if args.ticker: 4813 server.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4814 4815 else: 4816 server.CloseTrades([args.figi]) # close only one trade by FIGI 4817 4818 elif args.close_trades is not None: 4819 server.CloseTrades(args.close_trades) # close trades for list of tickers 4820 4821 elif args.close_all is not None: 4822 server.CloseAll(*args.close_all) 4823 4824 elif args.limits: 4825 if args.output is not None: 4826 server.withdrawalLimitsFile = args.output 4827 4828 server.OverviewLimits(show=True) 4829 4830 elif args.user_info: 4831 if args.output is not None: 4832 server.userInfoFile = args.output 4833 4834 server.OverviewUserInfo(show=True) 4835 4836 elif args.account: 4837 if args.output is not None: 4838 server.userAccountsFile = args.output 4839 4840 server.OverviewAccounts(show=True) 4841 4842 else: 4843 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4844 raise Exception("There is no command to execute") 4845 4846 except Exception: 4847 trace = tb.format_exc() 4848 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4849 if e in trace: 4850 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4851 break 4852 4853 uLogger.debug(trace) 4854 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4855 exitCode = 255 # an error occurred, must be open a ticket for this issue 4856 4857 finally: 4858 finish = datetime.now(tzutc()) 4859 4860 if exitCode == 0: 4861 uLogger.debug("All operations were finished success (summary code is 0).") 4862 4863 else: 4864 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4865 os.path.abspath(uLog.defaultLogFile), exitCode, 4866 )) 4867 4868 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4869 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4870 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4871 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4872 )) 4873 4874 if not kwargs: 4875 sys.exit(exitCode) 4876 4877 else: 4878 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: